Skip to content

Preferences

Preferences are Marko’s mechanism for replacing one concrete class with another globally. They’re the clean way to say “whenever the system creates X, give it my version instead.”

This is distinct from bindings, which map interfaces to implementations. Preferences swap class for class.

Use the #[Preference] attribute on your replacement class to declare what it replaces:

app/blog/src/Controller/CustomPostController.php
<?php
declare(strict_types=1);
namespace App\Blog\Controller;
use Marko\Blog\Controller\PostController;
use Marko\Core\Attributes\Preference;
#[Preference(replaces: PostController::class)]
class CustomPostController extends PostController
{
// Override specific methods as needed
}

Now everywhere the system would create PostController, it creates CustomPostController instead. No configuration files, no binding overrides — the attribute declares the intent right on the class.

Preferences are the right tool when you want to replace a concrete class:

  • Replace a vendor controller with your own
  • Swap a service class for a custom implementation
  • Override a model with extended behavior

If you’re swapping which implementation an interface resolves to, use a binding in module.php:

app/my-app/module.php
<?php
use Marko\Cache\CachePoolInterface;
use Marko\Cache\Redis\RedisCachePool;
return [
'bindings' => [
CachePoolInterface::class => RedisCachePool::class,
],
];
Use CaseTool
Replace a concrete class globallyPreference (#[Preference] attribute)
Swap an interface’s implementationBinding (module.php bindings)
Modify input/output of a specific methodPlugin
React to something happeningObserver

Preferences and bindings are coarse-grained (whole class replacement). Plugins are fine-grained (method-level interception). Use the right tool for the scope of your change.

Preferences can chain. If module A prefers PostControllerCustomPostController, and module B prefers CustomPostControllerAdvancedPostController, the container follows the chain to the final replacement.

Marko enforces explicit, deterministic Preference resolution — no silent “last one wins” behavior.

Same-priority conflict: If two modules at the same priority level (e.g., two vendor/ packages) both define a Preference for the same class, Marko throws a PreferenceConflictException naming both modules. You must resolve the conflict by moving one Preference to a higher-priority module or removing the duplicate.

Different priorities: Higher-priority modules override lower ones silently, as designed. app/ beats modules/ beats vendor/. This is the intended mechanism for customization.

This matches how binding conflicts work — same rules, same loud errors.

  1. Preferences replace concrete classes, not interfaces. For interface swapping, use bindings.
  2. Higher-priority modules win. app/ beats modules/ beats vendor/.
  3. Same-priority conflicts are errors. Two vendor packages can’t both prefer the same class.