PSR-14: Event Dispatcher
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches' (d5), phpstan/deptrac per detection_hints can detect tight coupling to framework-specific dispatchers vs. PSR-14 interface usage, but it requires configured rules.
Closest to 'touches multiple files / significant refactor in one component' (e5), swapping a framework event system to PSR-14 interface requires changing listener registration, event class signatures, and dispatch call sites across the event subsystem.
Closest to 'persistent productivity tax' (b5), since applies_to spans web/cli/queue contexts and events are typically dispatched throughout the codebase, the event abstraction shapes many work streams but isn't fully system-defining.
Closest to 'notable trap' (t5), per misconception developers assume PSR-14 covers async/queued dispatch and priorities, but it's strictly synchronous in-process — a documented gotcha most learn after hitting it.
Also Known As
TL;DR
Explanation
PSR-14 separates event dispatch (EventDispatcherInterface::dispatch($event)) from listener registration (ListenerProviderInterface::getListenersForEvent($event)). Any object can be an event; stoppable events implement StoppableEventInterface. This decouples producers from consumers and allows mixing listeners from different libraries. Implementations include Symfony EventDispatcher and league/event. The standard enables plugin architectures where third-party code attaches listeners without modifying the dispatching class — following Open/Closed Principle. Keep events as simple data-carrying objects; avoid placing behaviour in event classes.
Common Misconception
Why It Matters
Common Mistakes
- Not stopping propagation when an event has been fully handled — subsequent listeners process already-handled events.
- Listeners that throw exceptions without considering impact on other listeners in the chain.
- Not making event objects immutable — listeners that modify event data create hidden coupling.
- Using PSR-14 for high-throughput events — the overhead is fine for business events, not for thousands of events per second.
Code Examples
// Tightly coupled event dispatch without PSR-14:
class OrderService {
public function place(Order $o): void {
$this->save($o);
$this->mailer->send($o); // Direct call — coupled
$this->inventory->reserve($o); // Direct call — coupled
}
}
// PSR-14: dispatch(new OrderPlaced($o)) — listeners handle independently
// PSR-14 Event Dispatcher
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
// Define an event
class OrderPlaced {
public function __construct(
public readonly Order $order,
public readonly \DateTimeImmutable $placedAt = new \DateTimeImmutable(),
) {}
}
// Dispatch after successful order
$dispatcher->dispatch(new OrderPlaced($order));
// Register a listener
$provider->listen(OrderPlaced::class, function(OrderPlaced $event): void {
$mailer->sendConfirmation($event->order);
});
// PSR-14 compatible: Symfony EventDispatcher, League/Event, tightenco/collect