Files
michaelschiemer/docs/claude/event-system.md

23 KiB

Event System

This guide covers the event-driven architecture of the framework.

Overview

The framework provides a sophisticated event system built on two core components:

  • EventBus: Simple, lightweight interface for basic event dispatching
  • EventDispatcher: Full-featured implementation with handler management, priorities, and propagation control

Key Features:

  • Attribute-based event handler registration via #[OnEvent]
  • Priority-based handler execution
  • Event propagation control (stop propagation)
  • Support for inheritance and interface-based event matching
  • Automatic handler discovery via framework's attribute system
  • Domain events for business logic decoupling
  • Application lifecycle events for framework integration

EventBus vs EventDispatcher

EventBus Interface

Purpose: Simple contract for event dispatching

interface EventBus
{
    public function dispatch(object $event): void;
}

When to use:

  • Simple event dispatching without return values
  • Fire-and-forget events
  • When you don't need handler results
  • Dependency injection when you want interface-based type hints

Example Usage:

final readonly class OrderService
{
    public function __construct(
        private EventBus $eventBus
    ) {}

    public function createOrder(CreateOrderCommand $command): Order
    {
        $order = Order::create($command);

        // Fire event without caring about results
        $this->eventBus->dispatch(new OrderCreatedEvent($order));

        return $order;
    }
}

EventDispatcher Implementation

Purpose: Full-featured event system with handler management and results

Key Features:

  • Returns array of handler results
  • Priority-based handler execution (highest priority first)
  • Stop propagation support
  • Manual handler registration via addHandler() or listen()
  • Automatic handler discovery via #[OnEvent] attribute
  • Inheritance and interface matching

When to use:

  • Need handler return values for processing
  • Want priority control over execution order
  • Need to stop event propagation conditionally
  • Require manual handler registration
  • Building complex event workflows

Example Usage:

final readonly class PaymentProcessor
{
    public function __construct(
        private EventDispatcher $dispatcher
    ) {}

    public function processPayment(Payment $payment): PaymentResult
    {
        // Dispatch and collect results from all handlers
        $results = $this->dispatcher->dispatch(
            new PaymentProcessingEvent($payment)
        );

        // Process handler results
        foreach ($results as $result) {
            if ($result instanceof PaymentValidationFailure) {
                throw new PaymentException($result->reason);
            }
        }

        return new PaymentResult($payment, $results);
    }
}

Implementation Relationship

// EventDispatcher implements EventBus interface
final class EventDispatcher implements EventBus
{
    public function dispatch(object $event): array
    {
        // Full implementation with handler management
    }
}

// In DI Container
$container->singleton(EventBus::class, EventDispatcher::class);
$container->singleton(EventDispatcher::class, EventDispatcher::class);

Recommendation: Use EventDispatcher for type hints when you need full features, use EventBus interface when you want loose coupling and don't need results.

Domain Events

What are Domain Events?

Domain Events represent significant state changes or occurrences in your business domain. They enable loose coupling between domain components.

Characteristics:

  • Immutable (readonly classes)
  • Named in past tense (OrderCreated, UserRegistered, PaymentCompleted)
  • Contain only relevant domain data
  • No business logic (pure data objects)
  • Include timestamp for audit trail

Creating Domain Events

namespace App\Domain\Order\Events;

use App\Domain\Order\ValueObjects\OrderId;
use App\Framework\Core\ValueObjects\Timestamp;

final readonly class OrderCreatedEvent
{
    public readonly Timestamp $occurredAt;

    public function __construct(
        public OrderId $orderId,
        public UserId $userId,
        public Money $total,
        ?Timestamp $occurredAt = null
    ) {
        $this->occurredAt = $occurredAt ?? Timestamp::now();
    }
}

Best Practices:

  • Use Value Objects for event properties
  • Include timestamp for audit trail
  • Keep events focused (single responsibility)
  • Version events for backward compatibility
  • Document event payload in PHPDoc

Framework Lifecycle Events

The framework provides built-in events for application lifecycle:

ApplicationBooted

Triggered: After application bootstrap completes

final readonly class ApplicationBooted
{
    public function __construct(
        public \DateTimeImmutable $bootTime,
        public string $environment,
        public Version $version,
        public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
    ) {}
}

Use Cases:

  • Initialize services after boot
  • Start background workers
  • Setup scheduled tasks
  • Warm caches

BeforeHandleRequest

Triggered: Before HTTP request processing

final readonly class BeforeHandleRequest
{
    public function __construct(
        public Request $request,
        public Timestamp $timestamp,
        public array $context = []
    ) {}
}

Use Cases:

  • Request logging
  • Performance monitoring
  • Security checks
  • Request transformation

AfterHandleRequest

Triggered: After HTTP request processing

final readonly class AfterHandleRequest
{
    public function __construct(
        public Request $request,
        public Response $response,
        public Duration $processingTime,
        public Timestamp $timestamp,
        public array $context = []
    ) {}
}

Use Cases:

  • Response logging
  • Performance metrics
  • Analytics collection
  • Cleanup operations

Example: User Registration Event

namespace App\Domain\User\Events;

final readonly class UserRegisteredEvent
{
    public readonly Timestamp $occurredAt;

    public function __construct(
        public UserId $userId,
        public Email $email,
        public UserName $userName,
        ?Timestamp $occurredAt = null
    ) {
        $this->occurredAt = $occurredAt ?? Timestamp::now();
    }
}

// Dispatching the event
final readonly class UserService
{
    public function __construct(
        private UserRepository $repository,
        private EventBus $eventBus
    ) {}

    public function register(RegisterUserCommand $command): User
    {
        $user = User::create(
            $command->email,
            $command->userName,
            $command->password
        );

        $this->repository->save($user);

        // Dispatch domain event
        $this->eventBus->dispatch(
            new UserRegisteredEvent(
                $user->id,
                $user->email,
                $user->userName
            )
        );

        return $user;
    }
}

Event Handler Registration

Attribute-Based Registration

Primary Method: Use #[OnEvent] attribute for automatic discovery

use App\Framework\Core\Events\OnEvent;

final readonly class UserEventHandlers
{
    public function __construct(
        private EmailService $emailService,
        private Logger $logger
    ) {}

    #[OnEvent(priority: 100)]
    public function sendWelcomeEmail(UserRegisteredEvent $event): void
    {
        $this->emailService->send(
            to: $event->email,
            template: 'welcome',
            data: ['userName' => $event->userName->value]
        );
    }

    #[OnEvent(priority: 50)]
    public function logUserRegistration(UserRegisteredEvent $event): void
    {
        $this->logger->info('User registered', [
            'user_id' => $event->userId->toString(),
            'email' => $event->email->value,
            'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s')
        ]);
    }
}

Attribute Parameters:

  • priority (optional): Handler execution order (higher = earlier, default: 0)
  • stopPropagation (optional): Stop execution after this handler (default: false)

Priority-Based Execution

Handlers execute in priority order (highest to lowest):

#[OnEvent(priority: 200)]  // Executes first
public function criticalHandler(OrderCreatedEvent $event): void { }

#[OnEvent(priority: 100)]  // Executes second
public function highPriorityHandler(OrderCreatedEvent $event): void { }

#[OnEvent(priority: 50)]   // Executes third
public function normalHandler(OrderCreatedEvent $event): void { }

#[OnEvent]                 // Executes last (default priority: 0)
public function defaultHandler(OrderCreatedEvent $event): void { }

Use Cases for Priorities:

  • Critical (200+): Security checks, validation, fraud detection
  • High (100-199): Transaction logging, audit trail
  • Normal (50-99): Business logic, notifications
  • Low (0-49): Analytics, metrics, cleanup

Stop Propagation

Purpose: Prevent subsequent handlers from executing

#[OnEvent(priority: 100, stopPropagation: true)]
public function validatePayment(PaymentProcessingEvent $event): PaymentValidationResult
{
    $result = $this->validator->validate($event->payment);

    if (!$result->isValid()) {
        // Stop propagation - no further handlers execute
        return new PaymentValidationFailure($result->errors);
    }

    return new PaymentValidationSuccess();
}

#[OnEvent(priority: 50)]
public function processPayment(PaymentProcessingEvent $event): void
{
    // This handler only executes if validation passes
    // (previous handler didn't set stopPropagation result)
    $this->gateway->charge($event->payment);
}

Important: stopPropagation stops execution after the current handler completes.

Manual Handler Registration

Alternative: Register handlers programmatically

final readonly class EventInitializer
{
    public function __construct(
        private EventDispatcher $dispatcher
    ) {}

    #[Initializer]
    public function registerHandlers(): void
    {
        // Using listen() method
        $this->dispatcher->listen(
            OrderCreatedEvent::class,
            function (OrderCreatedEvent $event) {
                // Handle event
            },
            priority: 100
        );

        // Using addHandler() method
        $this->dispatcher->addHandler(
            eventClass: UserRegisteredEvent::class,
            handler: [$this->userService, 'onUserRegistered'],
            priority: 50
        );
    }
}

When to use manual registration:

  • Dynamic handler registration based on configuration
  • Third-party library integration
  • Closure-based handlers for simple cases
  • Runtime handler modification

Handler Discovery

The framework automatically discovers handlers marked with #[OnEvent]:

// In Application Bootstrap
final readonly class EventSystemInitializer
{
    #[Initializer]
    public function initialize(EventDispatcher $dispatcher): void
    {
        // Automatic discovery finds all #[OnEvent] methods
        $discoveredHandlers = $this->attributeScanner->findMethodsWithAttribute(
            OnEvent::class
        );

        foreach ($discoveredHandlers as $handler) {
            $dispatcher->addHandler(
                eventClass: $handler->getEventClass(),
                handler: [$handler->instance, $handler->method],
                priority: $handler->attribute->priority ?? 0
            );
        }
    }
}

No Manual Setup Required: Framework handles discovery automatically during initialization.

Event Middleware

Concept: Middleware pattern for event processing (transform, filter, log, etc.)

Custom Event Middleware

interface EventMiddleware
{
    public function process(object $event, callable $next): mixed;
}

final readonly class LoggingEventMiddleware implements EventMiddleware
{
    public function __construct(
        private Logger $logger
    ) {}

    public function process(object $event, callable $next): mixed
    {
        $eventClass = get_class($event);
        $startTime = microtime(true);

        $this->logger->debug("Event dispatched: {$eventClass}");

        try {
            $result = $next($event);

            $duration = (microtime(true) - $startTime) * 1000;
            $this->logger->debug("Event processed: {$eventClass}", [
                'duration_ms' => $duration
            ]);

            return $result;
        } catch (\Throwable $e) {
            $this->logger->error("Event failed: {$eventClass}", [
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }
}

Middleware Pipeline

final class EventDispatcherWithMiddleware
{
    /** @var EventMiddleware[] */
    private array $middleware = [];

    public function addMiddleware(EventMiddleware $middleware): void
    {
        $this->middleware[] = $middleware;
    }

    public function dispatch(object $event): array
    {
        $pipeline = array_reduce(
            array_reverse($this->middleware),
            fn($next, $middleware) => fn($event) => $middleware->process($event, $next),
            fn($event) => $this->eventDispatcher->dispatch($event)
        );

        return $pipeline($event);
    }
}

Validation Middleware

final readonly class ValidationEventMiddleware implements EventMiddleware
{
    public function __construct(
        private ValidatorInterface $validator
    ) {}

    public function process(object $event, callable $next): mixed
    {
        // Validate event before dispatching
        $errors = $this->validator->validate($event);

        if (!empty($errors)) {
            throw new InvalidEventException(
                "Event validation failed: " . implode(', ', $errors)
            );
        }

        return $next($event);
    }
}

Async Event Processing

Queue-Based Async Handling

use App\Framework\Queue\Queue;
use App\Framework\Queue\ValueObjects\JobPayload;

final readonly class AsyncEventHandler
{
    public function __construct(
        private Queue $queue
    ) {}

    #[OnEvent(priority: 50)]
    public function processAsync(OrderCreatedEvent $event): void
    {
        // Dispatch to queue for async processing
        $job = new ProcessOrderEmailJob(
            orderId: $event->orderId,
            userEmail: $event->userEmail
        );

        $this->queue->push(
            JobPayload::immediate($job)
        );
    }
}

// Background Job
final readonly class ProcessOrderEmailJob
{
    public function __construct(
        public OrderId $orderId,
        public Email $userEmail
    ) {}

    public function handle(EmailService $emailService): void
    {
        // Process email asynchronously
        $emailService->sendOrderConfirmation(
            $this->orderId,
            $this->userEmail
        );
    }
}

Event Buffering Pattern

final class EventBuffer
{
    private array $bufferedEvents = [];

    #[OnEvent]
    public function bufferEvent(DomainEvent $event): void
    {
        $this->bufferedEvents[] = $event;
    }

    public function flush(EventDispatcher $dispatcher): void
    {
        foreach ($this->bufferedEvents as $event) {
            $dispatcher->dispatch($event);
        }

        $this->bufferedEvents = [];
    }
}

// Usage in transaction
try {
    $this->entityManager->beginTransaction();

    // Buffer events during transaction
    $this->eventBuffer->bufferEvent(new OrderCreatedEvent(/* ... */));
    $this->eventBuffer->bufferEvent(new InventoryReservedEvent(/* ... */));

    $this->entityManager->commit();

    // Flush events after successful commit
    $this->eventBuffer->flush($this->dispatcher);
} catch (\Exception $e) {
    $this->entityManager->rollback();
    // Events are not flushed on rollback
    throw $e;
}

Event Best Practices

1. Event Naming

Good: Past tense, descriptive

OrderCreatedEvent
UserRegisteredEvent
PaymentCompletedEvent
InventoryReservedEvent

Bad: Present/future tense, vague

CreateOrderEvent
RegisterUserEvent
CompletePayment
ReserveInventory

2. Event Granularity

Focused Events:

final readonly class OrderCreatedEvent { /* ... */ }
final readonly class OrderShippedEvent { /* ... */ }
final readonly class OrderCancelledEvent { /* ... */ }

God Events:

final readonly class OrderEvent
{
    public function __construct(
        public string $action, // 'created', 'shipped', 'cancelled'
        public Order $order
    ) {}
}

3. Immutability

Readonly Events:

final readonly class UserUpdatedEvent
{
    public function __construct(
        public UserId $userId,
        public Email $newEmail
    ) {}
}

Mutable Events:

final class UserUpdatedEvent
{
    public UserId $userId;
    public Email $newEmail;

    public function setEmail(Email $email): void
    {
        $this->newEmail = $email; // Bad: Events should be immutable
    }
}

4. Handler Independence

Independent Handlers:

#[OnEvent]
public function sendEmail(OrderCreatedEvent $event): void
{
    // Self-contained - doesn't depend on other handlers
    $this->emailService->send(/* ... */);
}

#[OnEvent]
public function updateInventory(OrderCreatedEvent $event): void
{
    // Independent - no shared state with email handler
    $this->inventoryService->reserve(/* ... */);
}

Coupled Handlers:

private bool $emailSent = false;

#[OnEvent(priority: 100)]
public function sendEmail(OrderCreatedEvent $event): void
{
    $this->emailService->send(/* ... */);
    $this->emailSent = true; // Bad: Shared state
}

#[OnEvent(priority: 50)]
public function logEmailSent(OrderCreatedEvent $event): void
{
    if ($this->emailSent) { // Bad: Depends on other handler
        $this->logger->info('Email sent');
    }
}

5. Error Handling

Graceful Degradation:

#[OnEvent]
public function sendNotification(OrderCreatedEvent $event): void
{
    try {
        $this->notificationService->send(/* ... */);
    } catch (NotificationException $e) {
        // Log error but don't fail the entire event dispatch
        $this->logger->error('Notification failed', [
            'order_id' => $event->orderId->toString(),
            'error' => $e->getMessage()
        ]);
    }
}

6. Avoid Circular Events

Circular Dependency:

#[OnEvent]
public function onOrderCreated(OrderCreatedEvent $event): void
{
    // Bad: Dispatching event from event handler can cause loops
    $this->eventBus->dispatch(new OrderProcessedEvent($event->orderId));
}

#[OnEvent]
public function onOrderProcessed(OrderProcessedEvent $event): void
{
    // Bad: Creates circular dependency
    $this->eventBus->dispatch(new OrderCreatedEvent(/* ... */));
}

Use Command/Service Layer:

#[OnEvent]
public function onOrderCreated(OrderCreatedEvent $event): void
{
    // Good: Use service to perform additional work
    $this->orderProcessingService->processOrder($event->orderId);
}

7. Performance Considerations

Heavy Operations → Queue:

#[OnEvent]
public function generateInvoicePdf(OrderCreatedEvent $event): void
{
    // Heavy operation - push to queue
    $this->queue->push(
        JobPayload::immediate(
            new GenerateInvoiceJob($event->orderId)
        )
    );
}

Light Operations → Synchronous:

#[OnEvent]
public function logOrderCreation(OrderCreatedEvent $event): void
{
    // Light operation - execute immediately
    $this->logger->info('Order created', [
        'order_id' => $event->orderId->toString()
    ]);
}

Common Patterns

Event Sourcing Pattern

final class OrderEventStore
{
    #[OnEvent]
    public function appendEvent(DomainEvent $event): void
    {
        $this->eventStore->append([
            'event_type' => get_class($event),
            'event_data' => json_encode($event),
            'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s'),
            'aggregate_id' => $event->getAggregateId()
        ]);
    }
}

Saga Pattern

final readonly class OrderSaga
{
    #[OnEvent(priority: 100)]
    public function onOrderCreated(OrderCreatedEvent $event): void
    {
        // Step 1: Reserve inventory
        $this->inventoryService->reserve($event->items);
    }

    #[OnEvent(priority: 90)]
    public function onInventoryReserved(InventoryReservedEvent $event): void
    {
        // Step 2: Process payment
        $this->paymentService->charge($event->orderId);
    }

    #[OnEvent(priority: 80)]
    public function onPaymentCompleted(PaymentCompletedEvent $event): void
    {
        // Step 3: Ship order
        $this->shippingService->ship($event->orderId);
    }
}

CQRS Pattern

// Command Side - Dispatches Events
final readonly class CreateOrderHandler
{
    public function handle(CreateOrderCommand $command): Order
    {
        $order = Order::create($command);
        $this->repository->save($order);

        $this->eventBus->dispatch(
            new OrderCreatedEvent($order)
        );

        return $order;
    }
}

// Query Side - Maintains Read Model
#[OnEvent]
public function updateOrderReadModel(OrderCreatedEvent $event): void
{
    $this->orderReadModel->insert([
        'order_id' => $event->orderId->toString(),
        'user_id' => $event->userId->toString(),
        'total' => $event->total->toDecimal(),
        'status' => 'created'
    ]);
}

Framework Integration

With Queue System

#[OnEvent]
public function queueHeavyTask(OrderCreatedEvent $event): void
{
    $this->queue->push(
        JobPayload::immediate(
            new ProcessOrderAnalyticsJob($event->orderId)
        )
    );
}

With Scheduler

#[OnEvent]
public function scheduleReminder(OrderCreatedEvent $event): void
{
    $this->scheduler->schedule(
        'send-order-reminder-' . $event->orderId,
        OneTimeSchedule::at(Timestamp::now()->addDays(3)),
        fn() => $this->emailService->sendReminder($event->orderId)
    );
}

With Cache System

#[OnEvent]
public function invalidateCache(OrderCreatedEvent $event): void
{
    $this->cache->forget(
        CacheTag::fromString("user_{$event->userId}")
    );
}

Summary

The Event System provides:

  • Decoupled Architecture: Loose coupling via events
  • Flexible Handling: Priority-based, propagation control
  • Automatic Discovery: #[OnEvent] attribute registration
  • Multiple Patterns: Domain events, application events, async processing
  • Framework Integration: Works with Queue, Scheduler, Cache
  • Testing Support: Easy to unit test and integration test
  • Performance: Efficient handler execution with priority optimization

When to Use Events:

  • Decoupling domain logic from side effects (email, notifications)
  • Cross-module communication without tight coupling
  • Audit trail and event sourcing
  • Async processing of non-critical operations
  • CQRS and saga patterns

When NOT to Use Events:

  • Synchronous business logic requiring immediate results
  • Simple function calls (don't over-engineer)
  • Performance-critical paths (events have overhead)
  • Operations requiring transactional guarantees across handlers