Features
Events & Listeners
Decouple features with event-driven architecture
Build loosely coupled systems by emitting events and registering listeners to react.
Quick Start
1. Define an Event
namespace App\Events;
use Glueful\Events\Contracts\BaseEvent;
class UserRegisteredEvent extends BaseEvent
{
public function __construct(
public readonly int $userId,
public readonly string $email
) {
parent::__construct();
$this->setMetadata('source', 'registration');
}
}
2. Listen to Event
use Glueful\Events\Event;
use App\Events\UserRegisteredEvent;
Event::listen(UserRegisteredEvent::class, function($event) {
// Send welcome email (queue async)
$queue = app(\Glueful\Queue\QueueManager::class);
$queue->push(SendWelcomeEmailJob::class, ['user_id' => $event->userId]);
// Track in analytics (pseudo)
app('analytics')->track('user.registered', [
'user_id' => $event->userId
]);
});
3. Dispatch Event
use Glueful\Events\Event;
Event::dispatch(new UserRegisteredEvent($user->id, $user->email));
When to Use Events
Use events when:
- Multiple subsystems react to same action
- You want loose coupling
- Side effects are optional
- Fan-out to multiple handlers
Use direct calls when:
- Only one consumer exists
- Coupling is acceptable
- Action must be synchronous
Basic Usage
Dispatch Events
use Glueful\Events\Event;
// Dispatch with data
Event::dispatch(new OrderPlacedEvent($orderId, $total));
// Multiple listeners will execute
Register Listeners
// Closure listener
Event::listen(OrderPlacedEvent::class, function($event) {
logger()->info('Order placed', ['id' => $event->orderId]);
});
// Class listener (optionally via service container reference)
Event::listen(OrderPlacedEvent::class, SendOrderConfirmationListener::class);
// Or lazy via container: Event::listen(OrderPlacedEvent::class, '@send_order_confirmation:handle');
Event Subscribers
Group related listeners in a subscriber:
namespace App\Listeners;
use Glueful\Events\EventSubscriberInterface;
class UserLifecycleSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
UserRegisteredEvent::class => 'onRegistered',
UserUpdatedEvent::class => 'onUpdated',
UserDeletedEvent::class => 'onDeleted',
];
}
public function onRegistered(UserRegisteredEvent $event): void
{
// Send welcome email
}
public function onUpdated(UserUpdatedEvent $event): void
{
// Clear cache
}
public function onDeleted(UserDeletedEvent $event): void
{
// Cleanup data
}
}
Async Event Handling
Queue expensive listeners:
Event::listen(UserRegisteredEvent::class, function($event) {
// This listener runs synchronously
logger()->info('User registered');
});
Event::listen(UserRegisteredEvent::class, function($event) {
// Queue this expensive work
$queue = app(\Glueful\Queue\QueueManager::class);
$queue->push(ProcessNewUserJob::class, ['user_id' => $event->userId]);
});
Common Patterns
User Registration
// Event
class UserRegisteredEvent extends BaseEvent
{
public function __construct(
public readonly int $userId,
public readonly string $email,
public readonly string $name
) {
parent::__construct();
}
}
// Listeners
Event::listen(UserRegisteredEvent::class, function($event) {
// Send welcome email
$queue = app(\Glueful\Queue\QueueManager::class);
$queue->push(SendWelcomeEmailJob::class, ['user_id' => $event->userId]);
});
Event::listen(UserRegisteredEvent::class, function($event) {
// Track analytics
Analytics::track('user.registered', [
'email' => $event->email
]);
});
Event::listen(UserRegisteredEvent::class, function($event) {
// Create default preferences
db()->table('user_preferences')->insert([
'user_id' => $event->userId,
'notifications' => true
]);
});
// Dispatch
Event::dispatch(new UserRegisteredEvent(
$user->id,
$user->email,
$user->name
));
Order Processing
class OrderPlacedEvent extends BaseEvent
{
public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly float $total
) {
parent::__construct();
}
}
// Process payment
Event::listen(OrderPlacedEvent::class, function($event) {
app(\Glueful\Queue\QueueManager::class)
->push(ProcessPaymentJob::class, ['order_id' => $event->orderId]);
});
// Send confirmation
Event::listen(OrderPlacedEvent::class, function($event) {
app(\Glueful\Queue\QueueManager::class)
->push(SendOrderConfirmationJob::class, ['order_id' => $event->orderId]);
});
// Update inventory
Event::listen(OrderPlacedEvent::class, function($event) {
app(\Glueful\Queue\QueueManager::class)
->push(UpdateInventoryJob::class, ['order_id' => $event->orderId]);
});
Cache Invalidation
class PostUpdatedEvent extends BaseEvent
{
public function __construct(
public readonly string $postUuid
) {
parent::__construct();
}
}
Event::listen(PostUpdatedEvent::class, function($event) {
// Clear post cache
$cache = \Glueful\Cache\CacheFactory::create();
$cache->delete('post:' . $event->postUuid);
// Clear list caches
$cache->deletePattern('posts:*');
});
Built-in Framework Events
Glueful emits these events automatically:
HTTP Events
// Request received
use Glueful\Events\Http\RequestEvent;
Event::listen(RequestEvent::class, function($event) {
logger()->info('Request', [
'method' => $event->request->getMethod(),
'path' => $event->request->getPathInfo()
]);
});
// Response sent
use Glueful\Events\Http\ResponseEvent;
Event::listen(ResponseEvent::class, function($event) {
logger()->info('Response', [
'status' => $event->response->getStatusCode()
]);
});
Auth Events
// Login success
use Glueful\Events\Auth\SessionCreatedEvent;
Event::listen(SessionCreatedEvent::class, function($event) {
logger()->info('User logged in', ['user_id' => $event->userId]);
});
// Login failure
use Glueful\Events\Auth\AuthenticationFailedEvent;
Event::listen(AuthenticationFailedEvent::class, function($event) {
logger()->warning('Login failed', ['email' => $event->email]);
});
Cache Events
// Cache hit
use Glueful\Events\Cache\CacheHitEvent;
Event::listen(CacheHitEvent::class, function($event) {
// Track cache hit ratio
});
// Cache miss
use Glueful\Events\Cache\CacheMissEvent;
Event::listen(CacheMissEvent::class, function($event) {
// Track cache miss ratio
});
Database Events
// Query executed
use Glueful\Events\Database\QueryExecutedEvent;
Event::listen(QueryExecutedEvent::class, function($event) {
if ($event->time > 100) { // 100ms
logger()->warning('Slow query', [
'sql' => $event->sql,
'time' => $event->time
]);
}
});
Configuration
config/events.php
:
return [
'enabled' => env('EVENTS_ENABLED', true),
'listeners' => [
'cache_invalidation' => true,
'security_monitoring' => true,
'performance_monitoring' => true,
'audit_logging' => true,
],
'performance' => [
'enabled' => true,
'slow_threshold_ms' => 100,
'memory_threshold_mb' => 10,
],
];
Performance Tips
Keep Listeners Fast
// ✅ Good - quick logging
Event::listen(OrderPlacedEvent::class, function($event) {
logger()->info('Order placed', ['id' => $event->orderId]);
});
// ❌ Bad - slow API call
Event::listen(OrderPlacedEvent::class, function($event) {
// This blocks all other listeners!
$this->externalApi->notify($event->orderId);
});
// ✅ Good - queue slow work
Event::listen(OrderPlacedEvent::class, function($event) {
Queue::push(new NotifyExternalApiJob($event->orderId));
});
Minimal Payloads
// ✅ Good - small payload
class UserUpdatedEvent extends BaseEvent
{
public function __construct(
public readonly int $userId
) {
parent::__construct();
}
}
// ❌ Bad - large payload
class UserUpdatedEvent extends BaseEvent
{
public function __construct(
public readonly User $user, // Full object!
public readonly array $oldData,
public readonly array $newData
) {
parent::__construct();
}
}
Stop Propagation
Prevent further listeners from executing:
Event::listen(OrderPlacedEvent::class, function($event) {
if ($event->total > 10000) {
// Manual review required
$event->stopPropagation();
return;
}
});
Testing Events
use Glueful\Events\Event;
public function testUserRegistrationDispatchesEvent()
{
$dispatched = false;
Event::listen(UserRegisteredEvent::class, function($event) use (&$dispatched) {
$dispatched = true;
$this->assertEquals('[email protected]', $event->email);
});
// Register user
$this->post('/auth/register', [
'email' => '[email protected]',
'password' => 'secret'
]);
$this->assertTrue($dispatched);
}
Best Practices
Use Events for Side Effects
// Main action
public function createPost($data)
{
$post = $this->getConnection()->table('posts')->insert($data);
// Dispatch event for side effects
Event::dispatch(new PostCreatedEvent($post->uuid));
return $post;
}
// Side effects as listeners
Event::listen(PostCreatedEvent::class, function($event) {
$cache = \Glueful\Cache\CacheFactory::create();
$cache->delete('posts:latest');
});
Event::listen(PostCreatedEvent::class, function($event) {
app(\Glueful\Queue\QueueManager::class)
->push(NotifySubscribersJob::class, ['post_uuid' => $event->postUuid]);
});
Domain Events
// Good domain events
UserRegisteredEvent
OrderPlacedEvent
PaymentProcessedEvent
InvoiceGeneratedEvent
// Not events (these are commands)
RegisterUserEvent // Use: RegisterUserCommand
PlaceOrderEvent // Use: PlaceOrderCommand
Troubleshooting
Listeners not firing?
- Verify listener is registered
- Check event class name matches
- Ensure events are enabled in config
Performance issues?
- Profile slow listeners
- Queue expensive operations
- Reduce payload size
Events firing multiple times?
- Check for duplicate listener registrations
- Verify event is only dispatched once
Next Steps
- Queues & Jobs - Queue async listeners
- Notifications - Event-driven notifications
- Caching - Cache invalidation with events