monkeyscloud / monkeyslegion-events
High-performance PSR-14 event dispatcher with typed interceptors, attribute-based listeners, event sourcing, and dispatch metrics for the MonkeysLegion framework.
Package info
github.com/MonkeysCloud/MonkeysLegion-Events
pkg:composer/monkeyscloud/monkeyslegion-events
Requires
- php: ^8.4
- psr/event-dispatcher: ^1.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- psr/log: ^3.0 — Required for LoggingInterceptor PSR-3 logging
This package is auto-updated.
Last update: 2026-04-12 04:34:19 UTC
README
High-performance PSR-14 event dispatcher with typed interceptors, attribute-based listeners, event sourcing, circuit breakers, and dispatch metrics for the MonkeysLegion framework.
Installation
composer require monkeyscloud/monkeyslegion-events:^2.0
Features
| Feature | Description |
|---|---|
| PSR-14 Compliant | Full EventDispatcherInterface + StoppableEventInterface |
| 5 PHP Attributes | #[Listener], #[Subscriber], #[ListenWhen], #[BeforeEvent], #[AfterEvent] |
| Priority + FIFO | Higher priority runs first; equal priority preserves registration order |
| One-Shot Listeners | once() — auto-removed after first invocation |
| Wildcard Listeners | Pattern matching: User* catches UserCreated, UserDeleted, etc. |
| Event Subscribers | Multi-event handler classes (Laravel + Symfony parity) |
| Stoppable Events | $event->stopPropagation() halts the listener chain |
| Interceptor Pipeline | NOVEL — Before/After hooks (AOP-style) |
| Conditional Listeners | NOVEL — #[ListenWhen] with guard method |
| Circuit Breaker | NOVEL — Auto-disable failing listeners after N failures |
| Event Store/Replay | NOVEL — Record & replay events for testing/debugging |
| Dispatch Metrics | NOVEL — Per-event timing and count tracking |
| Correlation IDs | NOVEL — Hex-based event tracing across request scope |
| Safe Mode | Catch listener exceptions instead of crashing |
| Batch Dispatch | Dispatch multiple events in sequence |
| ShouldQueue Marker | Flag listeners for async/queue processing |
| ShouldBroadcast | Flag events for WebSocket/SSE broadcasting |
| PHP 8.4 Native | Property hooks, backed enums, asymmetric visibility |
Quick Start
use MonkeysLegion\Events\Event; use MonkeysLegion\Events\EventDispatcher; use MonkeysLegion\Events\ListenerProvider; // Define an event final class UserCreated extends Event { public function __construct( public readonly string $email, ) { parent::__construct(); } } // Register listeners $provider = new ListenerProvider(); $provider->add(UserCreated::class, function (UserCreated $event) { echo "Welcome, {$event->email}!"; }); // Dispatch $dispatcher = new EventDispatcher($provider); $dispatcher->dispatch(new UserCreated('jorge@monkeyscloud.com'));
Attribute-Based Listeners
use MonkeysLegion\Events\Attribute\Listener; #[Listener(event: UserCreated::class, priority: 10)] final class SendWelcomeEmail { public function __invoke(UserCreated $event): void { // Send email to $event->email } } // Register via attribute scanning $provider->addFromAttributes(new SendWelcomeEmail());
Conditional Listeners (Novel)
use MonkeysLegion\Events\Attribute\Listener; use MonkeysLegion\Events\Attribute\ListenWhen; #[Listener(event: OrderPlaced::class)] #[ListenWhen(method: 'isHighValue')] final class OnHighValueOrder { public function isHighValue(OrderPlaced $event): bool { return $event->amount > 1000; } public function __invoke(OrderPlaced $event): void { // Only called when amount > 1000 } }
Interceptor Pipeline (Novel)
AOP-style before/after hooks that wrap the regular listener chain:
use MonkeysLegion\Events\Attribute\BeforeEvent; use MonkeysLegion\Events\Attribute\AfterEvent; final class OrderInterceptor { #[BeforeEvent(event: OrderPlaced::class)] public function validate(OrderPlaced $event): void { // Runs BEFORE regular listeners } #[AfterEvent(event: OrderPlaced::class)] public function audit(OrderPlaced $event): void { // Runs AFTER all regular listeners } } $provider->addFromAttributes(new OrderInterceptor());
Global Interceptors
use MonkeysLegion\Events\Interceptor\TimingInterceptor; use MonkeysLegion\Events\Interceptor\LoggingInterceptor; use MonkeysLegion\Events\EventMetrics; $metrics = new EventMetrics(); $dispatcher->addInterceptor(new TimingInterceptor($metrics)); $dispatcher->addInterceptor(new LoggingInterceptor($psrLogger)); $dispatcher->dispatch(new OrderPlaced(42, 99.99)); echo $metrics->countFor(OrderPlaced::class); // 1 echo $metrics->averageFor(OrderPlaced::class); // 0.123 ms
Event Subscribers
use MonkeysLegion\Events\EventSubscriberInterface; final class UserEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ UserCreated::class => 'onUserCreated', UserDeleted::class => ['onUserDeleted', 10], // with priority OrderPlaced::class => [ ['onOrderLog', 10], ['onOrderNotify', 5], ], ]; } public function onUserCreated(UserCreated $event): void { /* ... */ } public function onUserDeleted(UserDeleted $event): void { /* ... */ } public function onOrderLog(OrderPlaced $event): void { /* ... */ } public function onOrderNotify(OrderPlaced $event): void { /* ... */ } } $provider->addSubscriber(new UserEventSubscriber());
Wildcard Listeners
// Matches UserCreated, UserDeleted, UserUpdated, etc. $provider->addWildcard('*UserCreated', function (object $event) { // Handle any event matching the pattern });
Stoppable Events
$provider->add(GenericEvent::class, function (Event $event) { $event->stopPropagation(); // Subsequent listeners won't run }, priority: 10); $provider->add(GenericEvent::class, function () { // This will NOT be called }, priority: 0);
Event Store & Replay (Novel)
use MonkeysLegion\Events\Store\EventStore; $store = new EventStore(); $dispatcher = new EventDispatcher($provider, store: $store); $dispatcher->dispatch(new UserCreated('a@test.com')); $dispatcher->dispatch(new OrderPlaced(1, 50.0)); // Query recorded events $users = $store->ofType(UserCreated::class); // [UserCreated] echo $store->size; // 2 // Replay all events through another dispatcher $store->replay($anotherDispatcher);
Dispatch Result & Metrics
$result = $dispatcher->dispatchWithResult(new UserCreated('test@test.com')); echo $result->listenersInvoked; // 3 echo $result->durationMs; // 0.456 echo $result->stopped; // false echo $result->isClean(); // true (no errors) // Batch dispatch $results = $dispatcher->dispatchBatch([ new UserCreated('a@test.com'), new OrderPlaced(1, 50.0), ]);
Circuit Breaker (Novel)
Listeners that fail repeatedly are auto-disabled:
$descriptor = new ListenerDescriptor( listener: fn() => throw new \RuntimeException('fail'), eventClass: OrderPlaced::class, circuitThreshold: 3, // Trip after 3 failures circuitResetTime: 60, // Try again after 60 seconds );
Safe Mode
// Catch listener exceptions instead of crashing $dispatcher = new EventDispatcher($provider, safeMode: true); $result = $dispatcher->dispatchWithResult(new OrderPlaced(1, 50.0)); // Errors captured in $result->errors instead of throwing
Async/Queue Markers
use MonkeysLegion\Events\Contract\ShouldQueue; #[Listener(event: OrderPlaced::class)] final class ProcessPayment implements ShouldQueue { public function __invoke(OrderPlaced $event): void { // Will be dispatched to queue by integration layer } }
Correlation Tracking
$event = new UserCreated('test@test.com'); echo $event->correlationId; // "a1b2c3d4..." (auto-generated 32-char hex correlation ID) echo $event->name; // "UserCreated" (auto-derived) echo $event->timestamp; // DateTimeImmutable
PHP 8.4 Features Used
| Feature | Where |
|---|---|
| Property Hooks | Event::$isPropagationStopped, Event::$name, EventMetrics, ListenerProvider::$count, EventStore::$size, EventDispatcher::$totalDispatches |
| Asymmetric Visibility | Event::$timestamp, Event::$correlationId, ListenerDescriptor |
| Backed Enum | EventType (Before/On/After) |
readonly classes |
DispatchResult, all Attributes |
match expressions |
EventType::label() |
new in initializers |
Event::$timestamp, ListenerDescriptor::$registeredAt |
| PHP 8 Attributes | 5 attributes across Attribute/ namespace |
Changelog
2.0.0 — Complete Rebuild
BREAKING CHANGE: Full API redesign from v1.
- Architecture: Replaced minimal PSR-14 shim with full interceptor-pipeline dispatcher
- 5 Attributes:
#[Listener],#[Subscriber],#[ListenWhen],#[BeforeEvent],#[AfterEvent] - Contracts:
ShouldQueue,ShouldBroadcast,EventSubscriberInterface - Interceptors:
InterceptorInterface,LoggingInterceptor,TimingInterceptor - Event Store: In-memory record & replay for testing/debugging/event sourcing
- Novel Features: Conditional listeners, circuit breaker, wildcard matching, correlation tracking, dispatch metrics, batch dispatch, safe mode
- PHP 8.4: Property hooks, backed enums, asymmetric visibility,
newin initializers - Tests: 59 tests, 119 assertions
Requirements
- PHP 8.4+
psr/event-dispatcher^1.0
Optional
psr/log^3.0 — ForLoggingInterceptor
License
MIT