Skip to content

re #97 PSR-14 object-based dispatcher for Altair\Happen#115

Merged
tonydspaniard merged 1 commit into
masterfrom
feat/97-psr14-happen
May 28, 2026
Merged

re #97 PSR-14 object-based dispatcher for Altair\Happen#115
tonydspaniard merged 1 commit into
masterfrom
feat/97-psr14-happen

Conversation

@tonydspaniard
Copy link
Copy Markdown
Member

Summary

Implements PSR-14's object-based interfaces in Altair\Happen alongside the existing name-keyed EventDispatcher — no BC break to the name-based dispatch surface. Closes #97 (Phase 3d, AGENT.md §7).

Design decision (the reason #97 was its own issue)

The issue offered two shapes. Option 2 (dual-mode, one class) is infeasible: a single class cannot declare a dispatch() that satisfies both Altair's dispatch(string $name, ?EventInterface $event): EventInterface and PSR-14's dispatch(object $event): object — same method name, incompatible signatures. So this ships option 1 (separate classes), which is also the cleanest separation: hosts bind whichever dispatcher they need.

What's added

  • Altair\Happen\Psr14EventDispatcherPsr\EventDispatcher\EventDispatcherInterface. Dispatches an event object to its matching listeners, returns the same instance, and honours StoppableEventInterface::isPropagationStopped() before each listener (so an already-stopped event invokes nothing, and a listener that stops halts the rest). final readonly.
  • Altair\Happen\Psr14ListenerProviderPsr\EventDispatcher\ListenerProviderInterface. Type-keyed registration via listen(class-string $eventType, callable $listener, int $priority); inheritance/interface-aware matching (a listener on a parent class or interface fires for subclasses); deterministic ordering — highest priority first, ties keep registration order via a global sequence counter. getListenersForEvent() snapshots and sorts before yielding, so a listener registered mid-dispatch does not fire in the same cycle.

The name-based EventDispatcher, Event, and all existing contracts are untouched. EventInterface already extended PSR-14's StoppableEventInterface, and composer.json already required psr/event-dispatcher.

Acceptance criteria

  • A class implements Psr\EventDispatcher\EventDispatcherInterface (dispatch(object): object, returns same instance)
  • A provider implements Psr\EventDispatcher\ListenerProviderInterface
  • Stoppable propagation halts further listeners on the PSR-14 path
  • Existing name-based API unchanged (no BC break)
  • Tests cover dispatch-by-object, propagation-stop, listener ordering (80%+ on new code)
  • composer qa + rector --dry-run green

Test plan

  • PHPStan level 8, zero baseline — [OK] No errors
  • Rector 2.x process --dry-run (full tree) — [OK] Rector is done!
  • php-cs-fixer fix --dry-run --using-cache=no — clean
  • Full Happen suite (51 tests) green; new PSR-14 tests cover dispatch-by-object, return-same-instance, priority + FIFO ordering, cross-type merge, inheritance + interface matching, already-stopped short-circuit, stop-mid-dispatch halt, and mid-dispatch registration isolation
  • CI confirms ext-dependent / 8.3 + 8.4 matrix (run locally on PHP 8.5 without some extensions)

Implements PSR-14's object-based interfaces alongside the existing
name-keyed EventDispatcher, with no BC break to the name-based surface.

A single class cannot satisfy both Altair's
dispatch(string, ?EventInterface): EventInterface and PSR-14's
dispatch(object): object (same method name, incompatible signatures), so
the standardization ships as separate classes (issue's design option 1):

- Psr14EventDispatcher implements Psr\EventDispatcher\EventDispatcherInterface:
  dispatches an event object to its listeners, returns the same instance, and
  honours StoppableEventInterface (checked before each listener, so an
  already-stopped event invokes nothing).
- Psr14ListenerProvider implements Psr\EventDispatcher\ListenerProviderInterface:
  type-keyed registration, inheritance/interface-aware matching, deterministic
  ordering (highest priority first, ties keep registration order via a global
  sequence counter). getListenersForEvent snapshots+sorts before yielding, so a
  listener registered mid-dispatch does not fire in the same cycle.

Tested: dispatch-by-object, propagation halt, already-stopped short-circuit,
priority + FIFO ordering, cross-type merge, inheritance/interface matching,
mid-dispatch registration isolation. composer qa + rector dry-run green.
@tonydspaniard tonydspaniard merged commit 1bb46c1 into master May 28, 2026
3 checks passed
@tonydspaniard tonydspaniard deleted the feat/97-psr14-happen branch May 28, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement PSR-14 dispatch in Altair\Happen (object-based, alongside name-based API)

1 participant