marko/core
The foundation of Marko — provides dependency injection, modules, plugins, events, and preferences so you can extend any class without modifying its source. Core gives you the extensibility primitives: replace any class with #[Preference], modify any method with #[Before]/#[After] plugins, react to events with #[Observer]. Everything is a module, and modules are discovered automatically from vendor/, modules/, and app/.
Installation
Section titled “Installation”composer require marko/coreNote: Most applications install this via a metapackage or implementation package.
Replacing Classes with Preferences
Section titled “Replacing Classes with Preferences”Override any class globally without touching its source:
use Marko\Core\Attributes\Preference;
#[Preference(replaces: OriginalService::class)]class MyService extends OriginalService{ public function doSomething(): string { // Your implementation return 'custom behavior'; }}Anywhere OriginalService is injected, MyService is provided instead.
Modifying Methods with Plugins
Section titled “Modifying Methods with Plugins”Intercept method calls without replacing the whole class:
use Marko\Core\Attributes\Plugin;use Marko\Core\Attributes\Before;use Marko\Core\Attributes\After;
#[Plugin(target: PaymentService::class)]class PaymentPlugin{ #[Before] public function beforeCharge( float $amount, ): ?float { // Modify input or return early return $amount * 1.1; // Add 10% fee }
#[After] public function afterCharge( Receipt $result, ): Receipt { // Modify output return $result->withTax(); }}Reacting to Events
Section titled “Reacting to Events”Decouple “something happened” from “react to it”:
use Marko\Core\Attributes\Observer;use Marko\Core\Event\Event;
#[Observer(event: 'user.created')]class SendWelcomeEmail{ public function handle( Event $event, ): void { $user = $event->data['user']; // Send email... }}Dispatch events from anywhere:
$this->eventDispatcher->dispatch('user.created', ['user' => $user]);Creating Modules
Section titled “Creating Modules”Create a directory in app/ with a composer.json:
app/ mymodule/ composer.json # Required: name, autoload module.php # Optional: enabled, bindings src/ MyService.phpModules are discovered automatically. Use module.php for bindings:
return [ 'enabled' => true, 'bindings' => [ PaymentInterface::class => StripePayment::class, ],];The boot callback runs after all module bindings are registered. Parameters are auto-injected from the container — type-hint any registered dependency:
return [ 'bindings' => [ PaymentInterface::class => StripePayment::class, ], 'boot' => function (ErrorHandlerInterface $handler): void { $handler->register(); },];Environment-Specific Bindings
Section titled “Environment-Specific Bindings”When different environments need different implementations (e.g., a mock service in development vs the real one in production), use the boot callback to conditionally override bindings:
return [ 'bindings' => [ // Default binding — used in all environments PaymentGatewayInterface::class => StripePaymentGateway::class, ], 'boot' => function (ContainerInterface $container): void { if (($_ENV['APP_ENV'] ?? 'production') === 'development') { $container->bind( PaymentGatewayInterface::class, MockPaymentGateway::class, ); } },];Since boot callbacks run after all static bindings are registered, $container->bind() in a boot callback overrides the static binding from the same module. The override is explicit and visible in the module’s own module.php.
Use boot callbacks when you need a completely different implementation class per environment (mock vs real).
Use config instead when the difference is just values (API URLs, credentials, feature flags). Keep the same class everywhere and let config drive the behavior:
return [ 'gateway_url' => $_ENV['PAYMENT_GATEWAY_URL'] ?? 'https://sandbox.stripe.com', 'dry_run' => (bool) ($_ENV['PAYMENT_DRY_RUN'] ?? true),];Throwing Rich Exceptions
Section titled “Throwing Rich Exceptions”Include context and fix suggestions:
use Marko\Core\Exceptions\MarkoException;
throw new MarkoException( message: 'Payment failed', context: 'Processing order #12345', suggestion: 'Check that the API key is configured in .env',);API Reference
Section titled “API Reference”Attributes
Section titled “Attributes”#[Preference(replaces: ClassName::class)] // Replace a class globally#[Plugin(target: ClassName::class)] // Mark class as plugin#[Before] // Run before target method#[After] // Run after target method#[Observer(event: 'event.name')] // React to events#[Command(name: 'cmd:name', description: '')] // Register CLI commandContainer
Section titled “Container”interface ContainerInterface{ public function get(string $id): mixed; public function has(string $id): bool; public function bind(string $abstract, string $concrete): void;}Events
Section titled “Events”interface EventDispatcherInterface{ public function dispatch(string|Event $event, array $data = []): Event;}MarkoException
Section titled “MarkoException”class MarkoException extends Exception{ public function __construct( string $message, string $context = '', string $suggestion = '', );
public function getContext(): string; public function getSuggestion(): string;}