Skip to content

PHP PSR-14 Event Dispatcher — Complete Guide to Event-Driven Architecture

DodaTech Updated 2026-06-28 4 min read

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

What is the difference between PSR-14 and Symfony EventDispatcher?

PSR-14 is a standard interface. Symfony EventDispatcher is an implementation that also implements PSR-14.

Can I use PSR-14 with Laravel?

Laravel has its own event system. You can bridge it with a PSR-14 adapter.

What is a stoppable event?

An event that can prevent further listener execution by setting a propagation stopped flag.

Are listeners called synchronously?

By default, yes. Listeners run in the order provided by the listener provider.

How do I pass data between listeners?

Use the event object itself as a shared mutable state. Keep it minimal to avoid coupling.

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