- 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
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()orlisten() - 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 (
readonlyclasses) - 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