memran / marwa-event
Lightweight PSR-14 event dispatcher for PHP 8.2+ with stable priority ordering, stoppable events, and optional container-aware resolution.
Requires
- php: >=8.2
- psr/container: ^2.0
- psr/event-dispatcher: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.89
- infection/infection: ^0.31
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.0
README
Lightweight PSR-14 event dispatching for PHP 8.2+ with predictable synchronous delivery, stable priority ordering, optional PSR-11 container integration, and a small framework-agnostic API.
Requirements
- PHP
>=8.2 - Composer
Installation
composer require memran/marwa-event
For local development:
composer install composer ci
Features
- PSR-14 aligned dispatcher and listener provider
- Stable priority ordering, including ties and mixed parent/interface listeners
- Flexible listener definitions: callable,
"Class@method",["Class", "method"], and invokable class strings - Optional PSR-11 container resolution for listeners and subscriber class strings
- Stoppable events via
Marwa\Event\Contracts\StoppableEvent - Fail-fast validation for invalid listener and subscriber definitions
- PHPUnit, PHPStan 2.x, PHP-CS-Fixer, GitHub Actions CI, and Infection setup
Quick Start
use Marwa\Event\Bus\EventBus; use Marwa\Event\Core\EventDispatcher; use Marwa\Event\Core\ListenerProvider; use Marwa\Event\Resolver\ListenerResolver; $resolver = new ListenerResolver(); $provider = new ListenerProvider($resolver); $dispatcher = new EventDispatcher($provider); $bus = new EventBus($provider, $dispatcher); $bus->listen(UserRegistered::class, SendWelcomeMail::class, 100); $bus->listen(UserRegistered::class, [AuditListener::class, 'handle'], 50); $bus->listen(UserRegistered::class, static function (UserRegistered $event): void { // metrics or notifications }, 10); $bus->dispatch(new UserRegistered('user@example.com'));
Examples
Define a Stoppable Event
use Marwa\Event\Contracts\StoppableEvent; final class UserRegistered extends StoppableEvent { public function __construct(public string $email) {} }
Register a Subscriber
use Marwa\Event\Contracts\Subscriber; final class UserSubscriber implements Subscriber { public static function getSubscribedEvents(): array { return [ UserRegistered::class => [ ['sendWelcome', 100], ['audit', 50], ], ]; } public function sendWelcome(UserRegistered $event): void {} public function audit(UserRegistered $event): void {} } $bus->subscribe(new UserSubscriber());
Remove a Listener
$listenerId = $bus->listen(UserRegistered::class, SendWelcomeMail::class, 100); $bus->forget($listenerId);
Listen on an Interface or Parent Event Type
interface DomainEvent {} class BaseOrderEvent implements DomainEvent {} final class OrderPlaced extends BaseOrderEvent {} $bus->listen(DomainEvent::class, LogDomainEvent::class, 100); $bus->listen(BaseOrderEvent::class, UpdateReadModel::class, 50); $bus->listen(OrderPlaced::class, SendOrderEmail::class, 10); $bus->dispatch(new OrderPlaced());
Use Container-Aware Resolution
use Marwa\Event\Bus\EventBus; use Marwa\Event\Core\EventDispatcher; use Marwa\Event\Core\ListenerProvider; use Marwa\Event\Resolver\ListenerResolver; $resolver = new ListenerResolver($container); $provider = new ListenerProvider($resolver); $dispatcher = new EventDispatcher($provider); $bus = new EventBus($provider, $dispatcher, $container); $bus->listen(UserRegistered::class, SendWelcomeMail::class); $bus->subscribe(UserSubscriber::class);
When a PSR-11 container is provided, listener and subscriber class strings are resolved from the container first.
Swallow Listener Exceptions Explicitly
$dispatcher = new EventDispatcher($provider, swallowExceptions: true); $bus = new EventBus($provider, $dispatcher);
The default remains false, which is usually the safer production choice.
Project Layout
src/
Bus/ Facade-style API
Contracts/ Shared interfaces and base event types
Core/ Dispatcher and listener provider
Resolver/ Listener notation resolution
tests/ PHPUnit test suite
example.php Minimal runnable example
Development Workflow
composer test # PHPUnit composer test:coverage # Coverage output; requires Xdebug or PCOV composer analyse # PHPStan 2.x composer mutate # Infection mutation testing; requires Xdebug, PCOV, or phpdbg composer lint # PHP-CS-Fixer dry run composer fix # PHP-CS-Fixer apply fixes composer ci # test + analyse + lint
Notes
- Higher priority values run first.
- Equal priorities keep registration order.
- Invalid listeners and subscribers throw
InvalidArgumentException. - Exceptions bubble by default unless
EventDispatcheris explicitly configured otherwise.
Quality and Release
The repository includes PHPUnit 10, PHPStan 2.x, PHP-CS-Fixer, GitHub Actions CI, and Infection configuration. Run composer ci before opening a PR or cutting a release.
Mutation testing is available through Infection. Run composer mutate after composer test when you want to measure how well the suite detects behavioral regressions. It requires Xdebug, PCOV, or phpdbg so Infection can collect coverage for the initial test pass.
Contributing
See AGENTS.md for repository-specific contributor guidance.