PHP PSR-14 Event Dispatcher — Complete Guide to Event-Driven Architecture
In this tutorial, you will learn about PHP PSR. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP PSR-14 defines a standard event dispatching system with EventDispatcherInterface for dispatching events, ListenerProviderInterface for registering listeners, and StoppableEventInterface.
What You'll Learn
By the end of this tutorial, you'll dispatch events, register listeners, create stoppable events, implement custom dispatchers, and build decoupled event-driven PHP applications.
Why PSR-14 Matters
Event-Driven Architecture decouples components. PSR-14 standardizes event dispatching, making libraries and applications interoperable across frameworks.
Real-World Use
An e-commerce application dispatches OrderPlaced events. Listeners send confirmation email, update inventory, notify analytics, and trigger shipping, all without the order controller knowing about them.
PSR-14 Path
flowchart LR
A[PSR-11 Container] --> B[PSR-14 Events]
B --> C[PSR-15 Middleware]
C --> D[PSR-17 HTTP Factory]
D --> E[PHP-DI Container]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Basic Event and Listener
Define an event class and a listener that handles it.
<?php
use Psr\EventDispatcher\StoppableEventInterface;
class OrderPlaced {
public function __construct(
public readonly int $orderId,
public readonly string $customerEmail,
public readonly float $total,
) {}
}
class SendOrderConfirmation {
public function __invoke(OrderPlaced $event): void {
echo "Sending confirmation to {$event->customerEmail} for order #{$event->orderId}\n";
}
}
Implementing a Dispatcher
Create a PSR-14 compatible event dispatcher.
<?php
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
class SimpleEventDispatcher implements EventDispatcherInterface {
public function __construct(
private ListenerProviderInterface $provider
) {}
public function dispatch(object $event): object {
foreach ($this->provider->getListenersForEvent($event) as $listener) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
break;
}
$listener($event);
}
return $event;
}
}
Listener Provider Implementation
Map event classes to their listeners.
<?php
use Psr\EventDispatcher\ListenerProviderInterface;
class SimpleListenerProvider implements ListenerProviderInterface {
private array $listeners = [];
public function addListener(string $eventClass, callable $listener): void {
$this->listeners[$eventClass][] = $listener;
}
public function getListenersForEvent(object $event): iterable {
$eventClass = get_class($event);
return $this->listeners[$eventClass] ?? [];
}
}
$provider = new SimpleListenerProvider();
$provider->addListener(OrderPlaced::class, new SendOrderConfirmation());
$provider->addListener(OrderPlaced::class, function (OrderPlaced $event) {
echo "Update inventory for order #{$event->orderId}\n";
});
Stoppable Events
Events can stop propagation when a listener determines no further processing is needed.
<?php
use Psr\EventDispatcher\StoppableEventInterface;
class CancellableOrderProcess implements StoppableEventInterface {
private bool $stopped = false;
public function __construct(
public readonly int $orderId,
public readonly string $reason = "",
) {}
public function stopPropagation(): void {
$this->stopped = true;
}
public function isPropagationStopped(): bool {
return $this->stopped;
}
}
$provider->addListener(CancellableOrderProcess::class, function ($event) {
if ($event->reason === "fraud") {
echo "Fraud detected, stopping order #{$event->orderId}\n";
$event->stopPropagation();
}
});
$provider->addListener(CancellableOrderProcess::class, function ($event) {
echo "This will not run if propagation was stopped\n";
});
Event-Driven Architecture
Dispatch events from domain logic to trigger side effects.
<?php
class OrderService {
public function __construct(
private EventDispatcherInterface $dispatcher
) {}
public function placeOrder(array $items): void {
$orderId = random_int(1000, 9999);
$this->dispatcher->dispatch(new OrderPlaced(
orderId: $orderId,
customerEmail: "customer@example.com",
total: 99.99
));
}
}
$provider = new SimpleListenerProvider();
$provider->addListener(OrderPlaced::class, fn($e) => echo "Email: {$e->customerEmail}\n");
$provider->addListener(OrderPlaced::class, fn($e) => echo "Inventory: {$e->orderId}\n");
$dispatcher = new SimpleEventDispatcher($provider);
$service = new OrderService($dispatcher);
$service->placeOrder(["item1", "item2"]);
Common Mistakes
1. Modifying Event Objects in Listeners
Events should be immutable. If listeners need to modify events, use a mutable event DTO Pattern.
2. Throwing Exceptions in Listeners
An exception in one listener prevents others from running. Wrap listener calls in try-catch.
3. Ignoring Listener Ordering
The PSR-14 spec does not guarantee order. Implement priority if ordering is required.
4. Creating Too Many Event Types
Too many granular events are hard to maintain. Group related events thoughtfully.
5. Not Using Stoppable Events for Conditional Processing
Use StoppableEventInterface when a listener can determine that subsequent listeners should not run.
Practice Questions
1. What does EventDispatcherInterface::dispatch return?
The event object after all applicable listeners have processed it.
2. What is the purpose of ListenerProviderInterface?
Provides listeners for a given event, enabling the dispatcher to call them.
3. How do you stop event propagation?
Implement StoppableEventInterface and call stopPropagation() in a listener.
4. Why should events be immutable?
Immutable events prevent listeners from interfering with each other by modifying shared state.
5. Challenge: Create an event system with priority-based listeners.
<?php
class PriorityListenerProvider implements ListenerProviderInterface {
private array $listeners = [];
public function addListener(string $eventClass, callable $listener, int $priority = 0): void {
$this->listeners[$eventClass][] = [$listener, $priority];
}
public function getListenersForEvent(object $event): iterable {
$eventClass = get_class($event);
$listeners = $this->listeners[$eventClass] ?? [];
usort($listeners, fn($a, $b) => $b[1] <=> $a[1]);
foreach ($listeners as $item) yield $item[0];
}
}
FAQ
Mini Project: Event-Driven Notification System
Build a notification system using PSR-14 events.
<?php
class UserRegistered {
public function __construct(
public readonly int $userId,
public readonly string $email,
public readonly string $name,
) {}
}
class SendWelcomeEmail {
public function __invoke(UserRegistered $event): void {
echo "Welcome email sent to {$event->email}\n";
}
}
class CreateUserDirectory {
public function __invoke(UserRegistered $event): void {
echo "Directory created for user {$event->name}\n";
}
}
$provider = new SimpleListenerProvider();
$provider->addListener(UserRegistered::class, new SendWelcomeEmail());
$provider->addListener(UserRegistered::class, new CreateUserDirectory());
$dispatcher = new SimpleEventDispatcher($provider);
$dispatcher->dispatch(new UserRegistered(1, "alice@example.com", "Alice"));
What's Next
PHP PSR-15 Middleware PHP PSR-17 HTTP Factory PHP Dependency Injection
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro