📦 Marketplace⭐ GitHub
Packagesv2.0

Events

PHP PSR-14 License

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

FeatureDescription
PSR-14 CompliantFull EventDispatcherInterface + StoppableEventInterface
5 PHP Attributes#[Listener], #[Subscriber], #[ListenWhen], #[BeforeEvent], #[AfterEvent]
Priority + FIFOHigher priority runs first; equal priority preserves registration order
One-Shot Listenersonce() — auto-removed after first invocation
Wildcard ListenersPattern matching: User* catches UserCreated, UserDeleted, etc.
Event SubscribersMulti-event handler classes (Laravel + Symfony parity)
Stoppable Events$event->stopPropagation() halts the listener chain
Interceptor PipelineNOVEL — Before/After hooks (AOP-style)
Conditional ListenersNOVEL#[ListenWhen] with guard method
Circuit BreakerNOVEL — Auto-disable failing listeners after N failures
Event Store/ReplayNOVEL — Record & replay events for testing/debugging
Dispatch MetricsNOVEL — Per-event timing and count tracking
Correlation IDsNOVEL — Hex-based event tracing across request scope
Safe ModeCatch listener exceptions instead of crashing
Batch DispatchDispatch multiple events in sequence
ShouldQueue MarkerFlag listeners for async/queue processing
ShouldBroadcastFlag events for WebSocket/SSE broadcasting
PHP 8.4 NativeProperty 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

FeatureWhere
Property HooksEvent::$isPropagationStopped, Event::$name, EventMetrics, ListenerProvider::$count, EventStore::$size, EventDispatcher::$totalDispatches
Asymmetric VisibilityEvent::$timestamp, Event::$correlationId, ListenerDescriptor
Backed EnumEventType (Before/On/After)
readonly classesDispatchResult, all Attributes
match expressionsEventType::label()
new in initializersEvent::$timestamp, ListenerDescriptor::$registeredAt
PHP 8 Attributes5 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, new in initializers
  • Tests: 59 tests, 119 assertions

Requirements

  • PHP 8.4+
  • psr/event-dispatcher ^1.0

Optional

  • psr/log ^3.0 — For LoggingInterceptor

License

MIT