skip to content

In PSR-14, how do EventDispatcherInterface and ListenerProviderInterface split the work of delivering an event to its listeners?

level: middleimportance: should knowfreq 30%

answer

  1. dispatch(object $event) returns the event
  2. getListenersForEvent(object $event): iterable
  3. provider picks listeners, never calls them
  4. synchronous, in provider order
  5. parent types count as matches

basics

~20 s

In PSR-14 a ListenerProvider decides which listeners apply to an event and in what order, but must not call them. The EventDispatcher asks the provider, calls each listener synchronously in that order, and returns the same event object.

solid answer

~40 s

PSR-14 splits dispatching into two roles. `ListenerProviderInterface::getListenersForEvent(object $event): iterable` returns the callables relevant to an event, in order, and MUST NOT call them; it should match by the event's class and must treat parent types as matches, so a listener typed against `A` applies to an event of subclass `B`. `EventDispatcherInterface::dispatch(object $event)` gets that list and MUST call the listeners synchronously in the order returned, MUST NOT return until all have run, and MUST return the same event object it was given. A listener is any callable with exactly one parameter, the event, and should return `void`; the dispatcher ignores return values, so listeners communicate back by changing a mutable event. Registration is left entirely to providers, which is why the standard defines no `addListener()`.

code

php · 33 lines
php
<?php
declare(strict_types=1);

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;

final class SimpleDispatcher implements EventDispatcherInterface
{
    public function __construct(private ListenerProviderInterface $provider) {}

    public function dispatch(object $event): object
    {
        foreach ($this->provider->getListenersForEvent($event) as $listener) {
            $listener($event); // synchronous, in provider order; return value ignored
        }
        return $event;         // the same object that was passed in
    }
}

final class ClassMapProvider implements ListenerProviderInterface
{
    /** @param array<class-string, list<callable>> $map */
    public function __construct(private array $map) {}

    public function getListenersForEvent(object $event): iterable
    {
        foreach ($this->map as $type => $listeners) {
            if ($event instanceof $type) { // parent types match too
                yield from $listeners;
            }
        }
    }
}

go deeper

for a junior

Remember that PSR-14 separates the dispatcher, which calls listeners, from the listener provider, which chooses them.

for a middle

Explain the dispatcher's obligations: synchronous calls in provider order, waiting for all listeners, returning the same event, and why registration lives in providers.

for a senior

Design libraries that ship their own listener providers and depend only on EventDispatcherInterface, and choose between mutable and immutable events deliberately.

for a principal

Decide where in-process events help decouple modules and where explicit calls or a message queue are clearer, given PSR-14's synchronous contract.

## The roles **PSR-14** standardises event dispatching in the `Psr\EventDispatcher` namespace. It names five roles: - **Event**: any PHP object, the message. - **Emitter**: the calling code that dispatches it. - **Listener**: any callable that expects to be passed an event. - **Dispatcher**: the service that passes the event to the relevant listeners. - **Listener provider**: the service that decides which listeners are relevant. The key design decision is that the dispatcher **must** defer the choice of listeners to a provider, and the provider **must not** call the listeners itself. ## The interfaces | Interface | Method | Responsibility | |---|---|---| | `EventDispatcherInterface` | `dispatch(object $event)` | call every relevant listener, return the event | | `ListenerProviderInterface` | `getListenersForEvent(object $event): iterable` | return the relevant callables, in order | | `StoppableEventInterface` | `isPropagationStopped(): bool` | let an event say it has been handled | `dispatch()` has no declared return type. The meta document explains that the only possible type would be `object`, which adds little, but returning the **same** event object is still a requirement. ## What the dispatcher must do 1. Call listeners **synchronously**, in the order the provider returned them. 2. **Not return** to the emitter until every listener has executed. 3. **Return the same event object** it was passed, which enables `$items = $dispatcher->dispatch(new CollectItems())->items();`. 4. Ignore listener return values. 5. Let any exception or `Error` thrown by a listener stop the remaining listeners and propagate to the emitter; it may catch one to log it, but must rethrow the original. A dispatcher should compose a provider as a separate object. Combining both in one class is allowed but not recommended. ## What the provider decides The provider chooses listeners and their order by any means it likes. The specification lists examples: explicit registration in a fixed order, reflection on listener signatures, a compiled list, access control, calling lifecycle methods on an entity referenced by the event, or delegating to other providers. Two rules constrain it: - providers **should** use the event's class name to tell events apart; - providers **must** treat parent types identically to the event's own type, so a listener declared as `function (A $event): void` applies to an instance of `B extends A`. The meta document explains why registration is not standardised: explicit registration, priorities, before/after ordering, containers, compile steps and conditional listeners all exist, and standardising one would cut off the rest. So a library that needs to add listeners ships its own provider, and the application aggregates providers. ## Listeners - A listener **must** have exactly one parameter, the event, and should type it as specifically as makes sense, possibly against an interface. - It **should** return `void`, since return values are ignored. - It may delegate to other code, or enqueue the event for later processing by a queue worker; changes made by that later process do **not** propagate back to other listeners. ## Events: mutable or not Events **may** be mutable when listeners need to pass information back to the emitter, for example collecting items or supplying a response. When no such back-channel is needed, the specification recommends immutable events without mutator methods. Implementations must assume the same event object reaches every listener. ## The scheduler example A reminder scheduler dispatches `ReminderDue` for each due reminder. One provider maps `ReminderDue` to a mail listener and a metrics listener. The scheduler depends only on `EventDispatcherInterface`; which listeners run, and in what order, is decided by the provider the application configured. Adding a push-notification listener changes the provider, not the scheduler. ## Common misunderstandings - Assuming PSR-14 dispatch is asynchronous: dispatch is synchronous, though a listener may enqueue work. - Returning a value from a listener to influence the emitter: it is ignored; change the event instead. - Looking for `addListener()` on the dispatcher: registration is provider-specific.

  • Why does PSR-14 not define an addListener() method?
    Its meta document found too many legitimate registration styles, explicit lists, priorities, before/after ordering, reflection, compiled code, container-based and conditional registration, to standardise one without cutting off the others. Registration is encapsulated behind `ListenerProviderInterface`, and each provider offers whatever mechanism it chooses.
  • How can a PSR-14 listener do slow work without delaying dispatch()?
    The dispatcher must call listeners synchronously, but a listener may enqueue information from the event, or the event itself if it serialises safely, for a queue worker to process later. That worker's changes to the event do not reach other listeners, so the listener gives up passing data back to the emitter.

saying these in an interview costs you the question

  • Says the listener provider calls the listeners itself.
  • Believes PSR-14 dispatchers run listeners asynchronously.
  • Expects dispatch() to return a new or cloned event.
  • Uses a listener's return value to send data back to the emitter.
  • Thinks a listener typed against a parent class will not receive subclass events.