Files
michaelschiemer/docs/features/events/system.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

964 lines
23 KiB
Markdown

# 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
```php
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**:
```php
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**:
```php
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
```php
// 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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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):
```php
#[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
```php
#[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
```php
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]`:
```php
// 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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
OrderCreatedEvent
UserRegisteredEvent
PaymentCompletedEvent
InventoryReservedEvent
```
**❌ Bad**: Present/future tense, vague
```php
CreateOrderEvent
RegisterUserEvent
CompletePayment
ReserveInventory
```
### 2. Event Granularity
**✅ Focused Events**:
```php
final readonly class OrderCreatedEvent { /* ... */ }
final readonly class OrderShippedEvent { /* ... */ }
final readonly class OrderCancelledEvent { /* ... */ }
```
**❌ God Events**:
```php
final readonly class OrderEvent
{
public function __construct(
public string $action, // 'created', 'shipped', 'cancelled'
public Order $order
) {}
}
```
### 3. Immutability
**✅ Readonly Events**:
```php
final readonly class UserUpdatedEvent
{
public function __construct(
public UserId $userId,
public Email $newEmail
) {}
}
```
**❌ Mutable Events**:
```php
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**:
```php
#[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**:
```php
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**:
```php
#[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**:
```php
#[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**:
```php
#[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**:
```php
#[OnEvent]
public function generateInvoicePdf(OrderCreatedEvent $event): void
{
// Heavy operation - push to queue
$this->queue->push(
JobPayload::immediate(
new GenerateInvoiceJob($event->orderId)
)
);
}
```
**Light Operations → Synchronous**:
```php
#[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
```php
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
```php
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
```php
// 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
```php
#[OnEvent]
public function queueHeavyTask(OrderCreatedEvent $event): void
{
$this->queue->push(
JobPayload::immediate(
new ProcessOrderAnalyticsJob($event->orderId)
)
);
}
```
### With Scheduler
```php
#[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
```php
#[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