Events API
Comprehensive guide to the event system in Forge Engine, including event dispatching, listeners, and event handling.
Introduction
The Forge Engine event system provides a powerful and flexible way to implement the observer pattern in your applications. Events allow you to decouple your application components and create more maintainable, testable code.
Event Dispatching
Events are dispatched throughout the application lifecycle, allowing other parts of your application to respond to specific occurrences.
Event Listening
Listeners are registered to handle specific events, enabling reactive programming patterns and loose coupling between components.
Event System Classes
Event
Base class for all events in the system.
use Forge\Events\Event;
class UserRegisteredEvent extends Event
{
    public function __construct(
        public readonly User $user,
        public readonly array $registrationData = []
    ) {}
    
    /**
     * Get event name
     */
    public function getName(): string
    {
        return 'user.registered';
    }
    
    /**
     * Stop event propagation
     */
    public function stopPropagation(): void
    {
        $this->propagationStopped = true;
    }
    
    /**
     * Check if propagation is stopped
     */
    public function isPropagationStopped(): bool
    {
        return $this->propagationStopped;
    }
}
// Usage
$event = new UserRegisteredEvent($user, $data);
echo $event->getName(); // 'user.registered'EventDispatcher
Central hub for event dispatching and listener management.
use Forge\Events\EventDispatcher;
$dispatcher = new EventDispatcher();
/**
 * Add event listener
 */
$dispatcher->addListener('user.registered', function(UserRegisteredEvent $event) {
    // Handle the event
    $user = $event->user;
    $this->sendWelcomeEmail($user);
});
/**
 * Add subscriber
 */
$dispatcher->addSubscriber(new UserEventSubscriber());
/**
 * Dispatch event
 */
$event = new UserRegisteredEvent($user, $data);
$dispatcher->dispatch($event, 'user.registered');
/**
 * Remove listener
 */
$dispatcher->removeListener('user.registered', $listener);
/**
 * Get listeners for event
 */
$listeners = $dispatcher->getListeners('user.registered');
/**
 * Check if has listeners
 */
if ($dispatcher->hasListeners('user.registered')) {
    // Event has listeners
}
/**
 * Priority-based listening
 */
$dispatcher->addListener('user.registered', $highPriorityListener, 100);
$dispatcher->addListener('user.registered', $lowPriorityListener, 10);EventSubscriber
Interface for classes that subscribe to multiple events.
use Forge\Events\EventSubscriberInterface;
class UserEventSubscriber implements EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to
     */
    public static function getSubscribedEvents(): array
    {
        return [
            'user.registered' => 'onUserRegistered',
            'user.logged_in' => 'onUserLoggedIn',
            'user.logged_out' => [
                ['onUserLoggedOut', 10],
                ['clearUserCache', 5],
            ],
            'user.password_changed' => [
                ['onPasswordChanged', 100],
                ['sendPasswordChangeNotification', 50],
            ],
        ];
    }
    
    public function onUserRegistered(UserRegisteredEvent $event): void
    {
        $user = $event->user;
        $this->sendWelcomeEmail($user);
        $this->createUserProfile($user);
    }
    
    public function onUserLoggedIn(UserLoggedInEvent $event): void
    {
        $this->updateLastLoginTime($event->user);
        $this->logUserActivity($event->user, 'login');
    }
    
    public function onUserLoggedOut(UserLoggedOutEvent $event): void
    {
        $this->logUserActivity($event->user, 'logout');
    }
    
    public function clearUserCache(UserLoggedOutEvent $event): void
    {
        Cache::forget('user.' . $event->user->id);
    }
    
    public function onPasswordChanged(PasswordChangedEvent $event): void
    {
        $this->logSecurityEvent($event->user, 'password_changed');
    }
    
    public function sendPasswordChangeNotification(PasswordChangedEvent $event): void
    {
        Mail::to($event->user->email)->send(new PasswordChangedNotification($event->user));
    }
}
// Register subscriber
$dispatcher->addSubscriber(new UserEventSubscriber());Event Dispatching
Basic Event Dispatching
Simple event dispatching with automatic listener execution.
use Forge\Events\EventDispatcher;
// Create event dispatcher
$dispatcher = new EventDispatcher();
// Define event class
class OrderPlacedEvent extends Event
{
    public function __construct(
        public readonly Order $order,
        public readonly User $user,
        public readonly float $totalAmount
    ) {}
    
    public function getName(): string
    {
        return 'order.placed';
    }
}
// Add listener
$dispatcher->addListener('order.placed', function(OrderPlacedEvent $event) {
    // Send order confirmation email
    Mail::to($event->user->email)->send(new OrderConfirmation($event->order));
    
    // Update inventory
    foreach ($event->order->items as $item) {
        $item->product->decrement('stock', $item->quantity);
    }
    
    // Log order activity
    Log::info('Order placed', [
        'order_id' => $event->order->id,
        'user_id' => $event->user->id,
        'amount' => $event->totalAmount
    ]);
});
// Dispatch event
$order = Order::create($orderData);
$event = new OrderPlacedEvent($order, $user, $totalAmount);
$dispatcher->dispatch($event, 'order.placed');Conditional Event Dispatching
Dispatch events based on conditions and stop propagation when needed.
class PaymentProcessedEvent extends Event
{
    private bool $propagationStopped = false;
    
    public function __construct(
        public readonly Payment $payment,
        public readonly string $status,
        public readonly ?string $errorMessage = null
    ) {}
    
    public function getName(): string
    {
        return 'payment.processed';
    }
    
    public function isSuccessful(): bool
    {
        return $this->status === 'success';
    }
    
    public function isFailed(): bool
    {
        return $this->status === 'failed';
    }
    
    public function stopPropagation(): void
    {
        $this->propagationStopped = true;
    }
    
    public function isPropagationStopped(): bool
    {
        return $this->propagationStopped;
    }
}
// Add validation listener (high priority)
$dispatcher->addListener('payment.processed', function(PaymentProcessedEvent $event) {
    if ($event->payment->amount <= 0) {
        $event->stopPropagation();
        Log::warning('Invalid payment amount', ['payment' => $event->payment->id]);
    }
}, 1000);
// Add processing listeners (normal priority)
$dispatcher->addListener('payment.processed', function(PaymentProcessedEvent $event) {
    if ($event->isSuccessful()) {
        // Update user balance
        $event->payment->user->increment('balance', $event->payment->amount);
        
        // Send success notification
        $this->sendPaymentSuccessNotification($event->payment);
    }
}, 100);
$dispatcher->addListener('payment.processed', function(PaymentProcessedEvent $event) {
    if ($event->isFailed()) {
        // Send failure notification
        $this->sendPaymentFailureNotification($event->payment, $event->errorMessage);
    }
}, 100);
// Add logging listener (low priority)
$dispatcher->addListener('payment.processed', function(PaymentProcessedEvent $event) {
    Log::info('Payment processed', [
        'payment_id' => $event->payment->id,
        'status' => $event->status,
        'amount' => $event->payment->amount
    ]);
}, 10);
// Dispatch event
$payment = Payment::create($paymentData);
$event = new PaymentProcessedEvent($payment, 'success');
$dispatcher->dispatch($event, 'payment.processed');Event Listeners
Closure Listeners
Simple closure-based event listeners for quick event handling.
// Simple closure listener
$dispatcher->addListener('user.created', function(UserCreatedEvent $event) {
    $user = $event->user;
    
    // Send welcome email
    Mail::to($user->email)->send(new WelcomeEmail($user));
    
    // Create user profile
    Profile::create([
        'user_id' => $user->id,
        'bio' => '',
        'avatar' => null
    ]);
    
    // Log user creation
    Log::info('User created', ['user_id' => $user->id, 'email' => $user->email]);
});
// Closure with dependency injection
$dispatcher->addListener('order.shipped', function(OrderShippedEvent $event) use ($trackingService) {
    $order = $event->order;
    
    // Generate tracking number
    $trackingNumber = $trackingService->generateTrackingNumber($order);
    
    // Update order with tracking info
    $order->update([
        'tracking_number' => $trackingNumber,
        'shipped_at' => now()
    ]);
    
    // Send shipping notification
    Mail::to($order->user->email)->send(new OrderShippedNotification($order, $trackingNumber));
});
// Multiple listeners for same event
$dispatcher->addListener('product.viewed', function(ProductViewedEvent $event) {
    // Increment view count
    $event->product->increment('view_count');
});
$dispatcher->addListener('product.viewed', function(ProductViewedEvent $event) {
    // Track user activity
    Activity::create([
        'user_id' => auth()->id(),
        'type' => 'product_view',
        'data' => ['product_id' => $event->product->id]
    ]);
});
$dispatcher->addListener('product.viewed', function(ProductViewedEvent $event) {
    // Update recommendation engine
    RecommendationEngine::trackProductView($event->product, auth()->user());
});Class-based Listeners
Object-oriented listeners with dependency injection and better organization.
class UserEventListener
{
    public function __construct(
        private EmailService $emailService,
        private NotificationService $notificationService,
        private LogService $logService
    ) {}
    
    public function onUserCreated(UserCreatedEvent $event): void
    {
        $user = $event->user;
        
        // Send welcome email
        $this->emailService->sendWelcomeEmail($user);
        
        // Send push notification
        $this->notificationService->sendWelcomeNotification($user);
        
        // Log the event
        $this->logService->logUserCreation($user);
    }
    
    public function onUserUpdated(UserUpdatedEvent $event): void
    {
        $user = $event->user;
        $changes = $event->changes;
        
        // Log profile changes
        if (isset($changes['profile'])) {
            $this->logService->logProfileUpdate($user, $changes['profile']);
        }
        
        // Send notification for important changes
        if (isset($changes['email'])) {
            $this->emailService->sendEmailChangeNotification($user, $changes['email']);
        }
    }
    
    public function onUserDeleted(UserDeletedEvent $event): void
    {
        $user = $event->user;
        
        // Clean up user data
        $this->cleanupUserData($user);
        
        // Log deletion
        $this->logService->logUserDeletion($user);
    }
    
    private function cleanupUserData(User $user): void
    {
        // Delete user files
        Storage::deleteDirectory("users/{$user->id}");
        
        // Delete user sessions
        Session::where('user_id', $user->id)->delete();
        
        // Anonymize user data
        $user->update([
            'name' => 'Deleted User',
            'email' => "deleted_{$user->id}@example.com",
            'deleted_at' => now()
        ]);
    }
}
// Register class-based listeners
$listener = new UserEventListener($emailService, $notificationService, $logService);
$dispatcher->addListener('user.created', [$listener, 'onUserCreated']);
$dispatcher->addListener('user.updated', [$listener, 'onUserUpdated']);
$dispatcher->addListener('user.deleted', [$listener, 'onUserDeleted']);
// Or use method names as strings
$dispatcher->addListener('user.created', 'UserEventListener@onUserCreated');
$dispatcher->addListener('user.updated', 'UserEventListener@onUserUpdated');Advanced Event Features
Event Wildcards
Listen to multiple events using wildcard patterns.
// Listen to all user events
$dispatcher->addListener('user.*', function(Event $event) {
    $eventName = $event->getName();
    Log::info("User event triggered: {$eventName}");
});
// Listen to all model events
$dispatcher->addListener('model.*', function(ModelEvent $event) {
    $model = $event->model;
    $action = $event->action; // 'created', 'updated', 'deleted'
    
    Log::info("Model {$action}", [
        'model' => get_class($model),
        'id' => $model->id
    ]);
});
// Listen to specific namespace events
$dispatcher->addListener('app.services.*', function(ServiceEvent $event) {
    $this->monitorServiceHealth($event->service);
});
// Complex wildcard patterns
$dispatcher->addListener('api.v1.users.*', function(ApiEvent $event) {
    $this->logApiActivity($event);
});
$dispatcher->addListener('payment.*.completed', function(PaymentEvent $event) {
    $this->processCompletedPayment($event);
});Async Event Handling
Handle events asynchronously using queues and background processing.
use Forge\Events\AsyncEventListener;
use Forge\Queue\QueueManager;
class AsyncUserEventListener extends AsyncEventListener
{
    public function __construct(
        private QueueManager $queue,
        private EmailService $emailService
    ) {}
    
    public function onUserRegistered(UserRegisteredEvent $event): void
    {
        // Queue welcome email
        $this->queue->push(new SendWelcomeEmailJob($event->user));
    }
    
    public function onUserPurchased(UserPurchasedEvent $event): void
    {
        // Queue purchase processing
        $this->queue->push(new ProcessPurchaseJob($event->purchase));
        
        // Queue analytics tracking
        $this->queue->push(new TrackPurchaseAnalyticsJob($event->purchase));
    }
}
// Register async listener
$asyncListener = new AsyncUserEventListener($queue, $emailService);
$dispatcher->addListener('user.registered', [$asyncListener, 'onUserRegistered']);
$dispatcher->addListener('user.purchased', [$asyncListener, 'onUserPurchased']);
// Async job implementation
class SendWelcomeEmailJob
{
    public function __construct(private User $user) {}
    
    public function handle(): void
    {
        Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
    }
}
// Deferred event handling
class DeferredEventListener
{
    public function onBulkImportStarted(BulkImportStartedEvent $event): void
    {
        // Defer heavy processing
        $this->defer(function() use ($event) {
            $this->processBulkImport($event->importData);
        });
    }
    
    private function defer(callable $callback): void
    {
        // Register shutdown function or use queue
        register_shutdown_function($callback);
    }
}Event Middleware
Intercept and modify events before they reach listeners.
interface EventMiddlewareInterface
{
    public function process(Event $event, callable $next): Event;
}
class EventValidationMiddleware implements EventMiddlewareInterface
{
    public function process(Event $event, callable $next): Event
    {
        // Validate event data
        if (!$this->validateEvent($event)) {
            throw new InvalidEventException('Event validation failed');
        }
        
        // Pass to next middleware
        return $next($event);
    }
    
    private function validateEvent(Event $event): bool
    {
        // Custom validation logic
        return true;
    }
}
class EventLoggingMiddleware implements EventMiddlewareInterface
{
    public function process(Event $event, callable $next): Event
    {
        // Log event before processing
        Log::info('Event processing started', [
            'event' => $event->getName(),
            'data' => $this->extractEventData($event)
        ]);
        
        $startTime = microtime(true);
        
        try {
            // Process event
            $result = $next($event);
            
            // Log successful processing
            $duration = microtime(true) - $startTime;
            Log::info('Event processed successfully', [
                'event' => $event->getName(),
                'duration' => $duration
            ]);
            
            return $result;
        } catch (\Exception $e) {
            // Log error
            Log::error('Event processing failed', [
                'event' => $event->getName(),
                'error' => $e->getMessage()
            ]);
            
            throw $e;
        }
    }
}
// Apply middleware to dispatcher
class MiddlewareEventDispatcher extends EventDispatcher
{
    private array $middleware = [];
    
    public function addMiddleware(EventMiddlewareInterface $middleware): void
    {
        $this->middleware[] = $middleware;
    }
    
    public function dispatch(Event $event, ?string $eventName = null): Event
    {
        $pipeline = array_reduce(
            array_reverse($this->middleware),
            function ($next, $middleware) {
                return function ($event) use ($next, $middleware) {
                    return $middleware->process($event, $next);
                };
            },
            function ($event) use ($eventName) {
                return parent::dispatch($event, $eventName);
            }
        );
        
        return $pipeline($event);
    }
}Common Event Patterns
Domain Events
Events that represent important business domain occurrences.
// Domain event base class
abstract class DomainEvent extends Event
{
    private DateTimeImmutable $occurredOn;
    
    public function __construct()
    {
        $this->occurredOn = new DateTimeImmutable();
    }
    
    public function getOccurredOn(): DateTimeImmutable
    {
        return $this->occurredOn;
    }
    
    abstract public function getAggregateId(): string;
}
// Specific domain events
class OrderPlacedDomainEvent extends DomainEvent
{
    public function __construct(
        private string $orderId,
        private string $customerId,
        private float $totalAmount,
        private array $items
    ) {
        parent::__construct();
    }
    
    public function getName(): string
    {
        return 'domain.order.placed';
    }
    
    public function getAggregateId(): string
    {
        return $this->orderId;
    }
    
    public function getOrderId(): string
    {
        return $this->orderId;
    }
    
    public function getCustomerId(): string
    {
        return $this->customerId;
    }
    
    public function getTotalAmount(): float
    {
        return $this->totalAmount;
    }
    
    public function getItems(): array
    {
        return $this->items;
    }
}
class PaymentReceivedDomainEvent extends DomainEvent
{
    public function __construct(
        private string $paymentId,
        private string $orderId,
        private float $amount,
        private string $paymentMethod
    ) {
        parent::__construct();
    }
    
    public function getName(): string
    {
        return 'domain.payment.received';
    }
    
    public function getAggregateId(): string
    {
        return $this->orderId;
    }
}
// Domain event subscriber
class DomainEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'domain.order.placed' => 'onOrderPlaced',
            'domain.payment.received' => 'onPaymentReceived',
        ];
    }
    
    public function onOrderPlaced(OrderPlacedDomainEvent $event): void
    {
        // Update inventory
        $this->updateInventory($event->getItems());
        
        // Notify warehouse
        $this->notifyWarehouse($event->getOrderId());
        
        // Update analytics
        $this->updateOrderAnalytics($event);
    }
    
    public function onPaymentReceived(PaymentReceivedDomainEvent $event): void
    {
        // Update order status
        $this->updateOrderStatus($event->getOrderId(), 'paid');
        
        // Trigger fulfillment process
        $this->triggerFulfillment($event->getOrderId());
    }
}Integration Events
Events for system integration and external service communication.
// Integration event for external services
class UserCreatedIntegrationEvent extends Event
{
    public function __construct(
        public readonly string $userId,
        public readonly string $email,
        public readonly string $name,
        public readonly array $metadata = []
    ) {}
    
    public function getName(): string
    {
        return 'integration.user.created';
    }
    
    public function toWebhookPayload(): array
    {
        return [
            'event' => 'user.created',
            'data' => [
                'id' => $this->userId,
                'email' => $this->email,
                'name' => $this->name,
                'timestamp' => now()->toIso8601String(),
            ],
            'metadata' => $this->metadata
        ];
    }
}
// Integration event subscriber
class IntegrationEventSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private WebhookService $webhookService,
        private QueueService $queueService,
        private ApiClient $apiClient
    ) {}
    
    public static function getSubscribedEvents(): array
    {
        return [
            'integration.user.created' => 'onUserCreated',
            'integration.order.placed' => 'onOrderPlaced',
            'integration.payment.processed' => 'onPaymentProcessed',
        ];
    }
    
    public function onUserCreated(UserCreatedIntegrationEvent $event): void
    {
        // Send webhook to CRM system
        $this->webhookService->send('crm', $event->toWebhookPayload());
        
        // Queue analytics sync
        $this->queueService->push(new SyncUserToAnalyticsJob($event->userId));
        
        // Notify external services
        $this->notifyExternalServices('user.created', $event);
    }
    
    private function notifyExternalServices(string $eventType, IntegrationEvent $event): void
    {
        $services = config('integration.services');
        
        foreach ($services as $service) {
            try {
                $this->apiClient->post($service['webhook_url'], $event->toWebhookPayload());
            } catch (\Exception $e) {
                Log::error("Failed to notify service {$service['name']}", [
                    'error' => $e->getMessage(),
                    'event' => $eventType
                ]);
            }
        }
    }
}
// Integration event factory
class IntegrationEventFactory
{
    public static function createFromDomainEvent(DomainEvent $domainEvent): ?IntegrationEvent
    {
        return match (true) {
            $domainEvent instanceof UserCreatedDomainEvent => new UserCreatedIntegrationEvent(
                $domainEvent->getUserId(),
                $domainEvent->getEmail(),
                $domainEvent->getName()
            ),
            $domainEvent instanceof OrderPlacedDomainEvent => new OrderPlacedIntegrationEvent(
                $domainEvent->getOrderId(),
                $domainEvent->getCustomerId(),
                $domainEvent->getTotalAmount()
            ),
            default => null
        };
    }
}Best Practices
Guidelines and recommendations for effectively using the event system in your Forge Engine applications.
Event Design Best Practices
Do's
- Use descriptive event names: Choose clear, action-oriented names like 'user.created' or 'order.shipped'
- Keep events focused: Each event should represent a single business occurrence
- Use immutable data: Make event properties readonly to prevent modification
- Include relevant context: Provide all necessary data for listeners to act
- Document events: Clearly document event structure and when they're triggered
Don'ts
- Don't create overly generic events: Avoid events like 'data.changed' that lack specificity
- Don't modify events in listeners: Events should be immutable after creation
- Don't make events dependent on specific listeners: Events should be independent of their consumers
- Don't include sensitive data: Avoid including passwords, tokens, or personal information
- Don't use events for direct method calls: Events should represent occurrences, not commands
Listener Best Practices
Do's
- Handle exceptions gracefully: Use try-catch blocks to prevent listener failures
- Use appropriate priorities: Set listener priorities based on execution order requirements
- Keep listeners focused: Each listener should have a single responsibility
- Make listeners idempotent: Ensure listeners can handle duplicate events safely
- Use dependency injection: Inject services rather than creating them in listeners
Don'ts
- Don't perform heavy operations synchronously: Use queues for time-consuming tasks
- Don't create circular dependencies: Avoid events that trigger other events in a loop
- Don't rely on listener execution order: Use priorities instead of assuming order
- Don't access the database directly: Use repositories or services
- Don't ignore performance: Profile and optimize slow listeners
Performance Considerations
Performance Tips
- Use lazy loading: Only load listeners when events are triggered
- Implement event caching: Cache compiled listener lists for better performance
- Use connection pooling: Reuse database connections in listeners
- Batch process events: Group similar events for bulk processing
- Monitor event performance: Track event dispatching and processing times
- Use selective listening: Only register listeners for events you actually handle