Phase 3d (AGENT.md §7). Altair\Happen predates PSR-14; this finishes the standardization.
Current state (partially done)
composer.json already requires psr/event-dispatcher: ^1.0.
Altair\Happen\Contracts\EventInterface already extends Psr\EventDispatcher\StoppableEventInterface.
- But the dispatcher and provider do not implement the PSR-14 interfaces:
EventDispatcher dispatches by name: dispatch(string $name, ?EventInterface $event): EventInterface.
Altair\Happen\Contracts\ListenerProviderInterface is Altair's own, keyed by string event name — not PSR-14's getListenersForEvent(object $event): iterable.
Goal
Implement PSR-14's object-based interfaces alongside the existing name-based API (no BC break to the current dispatch surface):
Psr\EventDispatcher\EventDispatcherInterface::dispatch(object $event): object
Psr\EventDispatcher\ListenerProviderInterface::getListenersForEvent(object $event): iterable
Design decision to make first (the reason this is its own issue)
Altair's model is name-keyed; PSR-14 is type-keyed. Two viable shapes — pick one and document why:
- Separate
Psr14EventDispatcher + type-keyed provider — cleanest separation; the existing name-based dispatcher stays untouched. Host apps choose which to bind.
- Dual-mode existing dispatcher —
EventDispatcher also accepts an object, routing by its class name; one class, two entry points. Less duplication, more coupling.
Either way: honour StoppableEventInterface::isPropagationStopped() on the PSR-14 path, and keep listener ordering deterministic.
Acceptance criteria
Note
Scoped out of the Phase-4 modernization push (the rest of which is done/tracked); deferred deliberately because it's a design choice baked into a public API, not a mechanical change.
Phase 3d (AGENT.md §7).
Altair\Happenpredates PSR-14; this finishes the standardization.Current state (partially done)
composer.jsonalready requirespsr/event-dispatcher: ^1.0.Altair\Happen\Contracts\EventInterfacealready extendsPsr\EventDispatcher\StoppableEventInterface.EventDispatcherdispatches by name:dispatch(string $name, ?EventInterface $event): EventInterface.Altair\Happen\Contracts\ListenerProviderInterfaceis Altair's own, keyed by string event name — not PSR-14'sgetListenersForEvent(object $event): iterable.Goal
Implement PSR-14's object-based interfaces alongside the existing name-based API (no BC break to the current dispatch surface):
Psr\EventDispatcher\EventDispatcherInterface::dispatch(object $event): objectPsr\EventDispatcher\ListenerProviderInterface::getListenersForEvent(object $event): iterableDesign decision to make first (the reason this is its own issue)
Altair's model is name-keyed; PSR-14 is type-keyed. Two viable shapes — pick one and document why:
Psr14EventDispatcher+ type-keyed provider — cleanest separation; the existing name-based dispatcher stays untouched. Host apps choose which to bind.EventDispatcheralso accepts an object, routing by its class name; one class, two entry points. Less duplication, more coupling.Either way: honour
StoppableEventInterface::isPropagationStopped()on the PSR-14 path, and keep listener ordering deterministic.Acceptance criteria
Psr\EventDispatcher\EventDispatcherInterface(dispatch(object): object, returns the same instance)Psr\EventDispatcher\ListenerProviderInterfacecomposer qa+rector --dry-rungreenNote
Scoped out of the Phase-4 modernization push (the rest of which is done/tracked); deferred deliberately because it's a design choice baked into a public API, not a mechanical change.