Events
Melodic includes a lightweight event system for decoupling components. The EventDispatcher lets you register listeners for specific event classes and dispatch events with priority ordering and propagation control.
Event Base Class
All events should extend the abstract Melodic\Event\Event class, which provides propagation control. You can also dispatch any plain object, but only subclasses of Event support stopPropagation().
<?php
namespace Melodic\Event;
abstract class Event
{
private bool $propagationStopped = false;
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}
| Method | Return Type | Description |
|---|---|---|
isPropagationStopped() |
bool |
Returns whether propagation has been stopped for this event |
stopPropagation() |
void |
Prevents any remaining listeners from being called |
Creating Custom Events
Define your own event classes by extending Event. Add any properties your listeners will need as constructor arguments.
<?php
use Melodic\Event\Event;
class UserRegistered extends Event
{
public function __construct(
public readonly int $userId,
public readonly string $email,
public readonly string $name,
) {}
}
class OrderPlaced extends Event
{
public function __construct(
public readonly int $orderId,
public readonly float $total,
public readonly int $customerId,
) {}
}
class PaymentFailed extends Event
{
public function __construct(
public readonly int $orderId,
public readonly string $reason,
) {}
}
Use readonly properties. Events should be immutable data carriers. Use constructor promotion with readonly to enforce this.
EventDispatcher API
The Melodic\Event\EventDispatcherInterface defines two methods. The concrete EventDispatcher implements both.
| Method | Signature | Description |
|---|---|---|
listen |
(string $eventClass, callable $listener, int $priority = 0): void |
Registers a listener for the given event class with optional priority |
dispatch |
(object $event): object |
Dispatches an event to all registered listeners, returns the event |
Registering Listeners
Call listen() with the fully-qualified event class name, a callable, and an optional priority. The callable receives the event object as its only argument.
<?php
use Melodic\Event\EventDispatcher;
$dispatcher = new EventDispatcher();
// Register listeners with closures
$dispatcher->listen(UserRegistered::class, function (UserRegistered $event) {
// Send welcome email
$mailer->sendWelcome($event->email, $event->name);
});
// Register with a class method
$dispatcher->listen(UserRegistered::class, [$auditLogger, 'onUserRegistered']);
// Register with an invokable class
$dispatcher->listen(OrderPlaced::class, new SendOrderConfirmation());
Dispatching Events
Create an event instance and pass it to dispatch(). The dispatcher returns the event object after all listeners have run, so you can inspect any state changes made by listeners.
<?php
// In your service layer
class UserService extends Service
{
public function __construct(
DbContextInterface $context,
private readonly EventDispatcherInterface $dispatcher,
) {
parent::__construct($context);
}
public function register(string $name, string $email, string $password): User
{
$user = (new CreateUserCommand($name, $email, $password))
->execute($this->context);
// Dispatch the event after the user is created
$this->dispatcher->dispatch(new UserRegistered(
userId: $user->id,
email: $user->email,
name: $user->name,
));
return $user;
}
}
Priority Ordering
Listeners can be assigned a priority when registered. Higher priority values run first. Listeners with the same priority run in the order they were registered.
<?php
// Priority 100 runs first, then 10, then 0 (default)
$dispatcher->listen(UserRegistered::class, function ($event) {
echo "Third (priority 0)\n";
}, priority: 0);
$dispatcher->listen(UserRegistered::class, function ($event) {
echo "First (priority 100)\n";
}, priority: 100);
$dispatcher->listen(UserRegistered::class, function ($event) {
echo "Second (priority 10)\n";
}, priority: 10);
$dispatcher->dispatch(new UserRegistered(1, 'a@b.com', 'Alice'));
// Output:
// First (priority 100)
// Second (priority 10)
// Third (priority 0)
Priority sorting. Internally, listeners are stored by priority key and sorted using krsort() (reverse numeric sort). This means higher numeric values execute before lower ones. The default priority is 0.
Stopping Propagation
If your event extends the Event base class, any listener can call stopPropagation() to prevent subsequent listeners from being called.
<?php
$dispatcher->listen(PaymentFailed::class, function (PaymentFailed $event) {
// This listener runs first (priority 100)
if ($event->reason === 'fraud') {
// Block the order and stop all other listeners
blockOrder($event->orderId);
$event->stopPropagation();
}
}, priority: 100);
$dispatcher->listen(PaymentFailed::class, function (PaymentFailed $event) {
// This listener will NOT run if propagation was stopped above
retryPayment($event->orderId);
}, priority: 0);
Plain objects. If you dispatch a plain object that does not extend Event, stopPropagation() is not available. The dispatcher checks for the Event base class before inspecting propagation status.
EventServiceProvider
The Melodic\Event\EventServiceProvider registers the event dispatcher as a singleton in the DI container. This ensures all components share the same dispatcher instance.
<?php
use Melodic\Core\Application;
use Melodic\Event\EventServiceProvider;
$app = new Application(__DIR__);
$app->services(function ($container) {
// Register the event service provider
(new EventServiceProvider())->register($container);
});
// Now EventDispatcherInterface resolves to a shared EventDispatcher
// The container will auto-wire it into any constructor that type-hints it
After registration, any class that depends on EventDispatcherInterface will automatically receive the shared dispatcher through constructor injection:
<?php
use Melodic\Event\EventDispatcherInterface;
class NotificationService
{
public function __construct(
private readonly EventDispatcherInterface $dispatcher,
) {}
public function registerListeners(): void
{
$this->dispatcher->listen(UserRegistered::class, function ($event) {
// Send notification...
});
}
}
Real-World Patterns
Decoupling Side Effects
Events are ideal for side effects that should not block the main operation. The service completes its core work, then fires an event for any secondary concerns.
<?php
// In your bootstrap or service provider, wire up all listeners
$dispatcher->listen(UserRegistered::class, function (UserRegistered $event) {
// Send welcome email
$mailer->send($event->email, 'Welcome!', "Hello {$event->name}");
}, priority: 10);
$dispatcher->listen(UserRegistered::class, function (UserRegistered $event) {
// Log the registration
$logger->info('New user registered', ['userId' => $event->userId]);
}, priority: 0);
$dispatcher->listen(UserRegistered::class, function (UserRegistered $event) {
// Sync to analytics platform
$analytics->track('user.registered', ['email' => $event->email]);
}, priority: 0);
Event-Driven Workflows
Chain events to build workflows where one action triggers the next:
<?php
// When an order is placed, check inventory
$dispatcher->listen(OrderPlaced::class, function (OrderPlaced $event) use ($dispatcher) {
$available = checkInventory($event->orderId);
if ($available) {
$dispatcher->dispatch(new OrderConfirmed($event->orderId));
} else {
$dispatcher->dispatch(new OrderBackordered($event->orderId));
}
});