waffle-commons/event-dispatcher

Event Dispatcher component for Waffle framework.

Maintainers

Package info

github.com/waffle-commons/event-dispatcher

pkg:composer/waffle-commons/event-dispatcher

Statistics

Installs: 12

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.0-beta2.1 2026-05-30 19:00 UTC

README

Discord PHP Version Require PHP CI codecov Latest Stable Version Latest Unstable Version Total Downloads Packagist License

Waffle Event Dispatcher Component

Release: v0.1.0-beta2 ย |ย  CHANGELOG.md PSR Compliance: PSR-14 (Psr\EventDispatcher\EventDispatcherInterface, ListenerProviderInterface, StoppableEventInterface)

A minimal, attribute-driven PSR-14 dispatcher. The dispatcher itself is final readonly and stateless; the listener provider stores the listener map and supports priority ordering and #[AsEventListener] attribute discovery.

๐Ÿ“ฆ Installation

composer require waffle-commons/event-dispatcher

๐Ÿงฑ Surface

Class Role
Waffle\Commons\EventDispatcher\Dispatcher\EventDispatcher final readonly PSR-14 dispatcher. Walks listeners, respects StoppableEventInterface.
Waffle\Commons\EventDispatcher\Provider\ListenerProvider Listener registry. Manual registration via addListener(), or attribute scanning via register($object).
Waffle\Commons\EventDispatcher\Attribute\AsEventListener PHP 8 attribute marking a class or method as a listener.
Waffle\Commons\EventDispatcher\Event\AbstractStoppableEvent Convenience base implementing StoppableEventInterface.

๐Ÿš€ Manual registration

use Waffle\Commons\EventDispatcher\Dispatcher\EventDispatcher;
use Waffle\Commons\EventDispatcher\Provider\ListenerProvider;

$provider = new ListenerProvider();
$provider->addListener(UserRegistered::class, function (UserRegistered $event): void {
    // โ€ฆ
}, priority: 100); // higher priority = earlier

$dispatcher = new EventDispatcher($provider);
$event = $dispatcher->dispatch(new UserRegistered($userId));

๐Ÿท๏ธ Attribute-driven registration (#[AsEventListener])

The attribute is declared as:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final readonly class AsEventListener
{
    public function __construct(
        public ?string $event = null,
        public int $priority = 0,
    ) {}
}

Method-level โ€” event class resolved from the parameter type-hint

final class AuditListener
{
    #[AsEventListener(priority: 50)]
    public function onUserRegistered(UserRegistered $event): void
    {
        // resolved automatically from the parameter type
    }
}

$provider->register(new AuditListener());

Class-level โ€” first public non-constructor method is the handler

#[AsEventListener(event: UserRegistered::class, priority: 50)]
final class WelcomeMailer
{
    public function send(UserRegistered $event): void { /* โ€ฆ */ }
}

$provider->register(new WelcomeMailer());

๐Ÿ›‘ Stoppable events

use Waffle\Commons\EventDispatcher\Event\AbstractStoppableEvent;

final class CancellableJob extends AbstractStoppableEvent
{
    public function __construct(public readonly string $jobId) {}
}

$provider->addListener(CancellableJob::class, function (CancellableJob $e): void {
    if ($shouldCancel) {
        $e->stopPropagation();
    }
});

The dispatcher honours isPropagationStopped() and breaks out of the listener loop.

๐Ÿ˜ PHP 8.5 features used

  • final readonly class EventDispatcher โ€” the dispatcher itself is immutable.
  • Constructor property promotion with explicit visibility on listeners.
  • Typed properties + parameters throughout.
  • Inheritance walks via native get_parent_class() (not reflection caches), so listener resolution against parent event types is O(depth) without warm-up cost.

๐Ÿงญ Architectural boundary (mago guard)

An active dependency perimeter is enforced on every CI run by vendor/bin/mago guard (bundled into composer mago; zero baselines). The rules live in mago.toml under [guard.perimeter] โ€” a forbidden use statement fails the build, not a reviewer.

Production code under Waffle\Commons\EventDispatcher may depend only on:

  • Waffle\Commons\EventDispatcher\** โ€” itself
  • Waffle\Commons\Contracts\** โ€” the shared contracts package, the only Waffle dependency permitted
  • Psr\** โ€” PSR interfaces (PSR-14)
  • @global + Psl\** โ€” PHP core and the PHP Standard Library

Test code under WaffleTests\Commons\EventDispatcher is unrestricted (@all). Structural rules are guarded too: interfaces must be named *Interface, Exception\** classes must end in *Exception, and any Enum\** namespace may hold only enum declarations.

Contract-first, component-agnostic by construction: components compose through waffle-commons/contracts, never directly through one another.

๐Ÿงช Testing

docker exec -w /waffle-commons/event-dispatcher waffle-dev composer tests

๐Ÿ“„ License

MIT โ€” see LICENSE.md.