Events & Listeners
Decouple 'something happened' from 'what to do about it' — the observer pattern, Laravel-style.
Events let a part of your app announce that something happened without knowing or caring who reacts. An OrderShipped event might trigger an email, a Slack ping, and an analytics update — three listeners, added and removed independently, with the code that ships the order none the wiser.
The two halves
- Event — a plain data-carrier class describing what happened.
- Listener — a class with a
handle()that reacts. ImplementShouldQueueto run it in the background.
class OrderShipped { public function __construct(public Order $order) {} }
class SendShipmentNotification
{
public function handle(OrderShipped $event): void { /* email */ }
}
event(new OrderShipped($order));Discovery & wiring
Laravel auto-discovers listeners by their type-hinted handle() argument, or you register them explicitly. Model events, cache events, and queue events also flow through this same system — you can listen to the framework's own signals.
When NOT to use events
Events add indirection. If exactly one thing must happen and it must happen now, a direct method call is clearer. Reach for events when there are multiple, optional, or cross-cutting reactions.
Example
<?php
// --- The event: a pure data carrier ---
namespace App\Events;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
}
// --- A queued listener: slow work, off the request cycle ---
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class NotifyCustomer implements ShouldQueue
{
use InteractsWithQueue;
public int $tries = 3;
public function handle(OrderShipped $event): void
{
$event->order->customer->notify(
new \App\Notifications\ShipmentDispatched($event->order)
);
}
// Called if the listener ultimately fails.
public function failed(OrderShipped $event, \Throwable $e): void
{
logger()->error('Ship notice failed', ['order' => $event->order->id]);
}
}
// --- Firing it (after commit, so it can't outrun the DB) ---
// OrderShipped::dispatch($order); // or event(new OrderShipped($order));
// Auto-discovered by the OrderShipped type-hint on handle().When to use it
- An OrderPlaced event is fired after a successful checkout, and multiple listeners (SendInvoiceEmail, UpdateInventory, NotifyWarehouse) handle it independently without coupling the checkout controller to any of them.
- A UserRegistered event triggers a welcome email listener and an analytics-tracking listener, so adding a third listener requires no changes to the registration controller.
- An integration test uses Event::fake() to assert that OrderPlaced was fired with the correct order payload without executing any real listeners during the test.
More examples
Define an event and a listener
Artisan creates the event class and a listener pre-typed with a handle(OrderPlaced $event) signature, wired together in EventServiceProvider.
php artisan make:event OrderPlaced
php artisan make:listener SendInvoiceEmail --event=OrderPlacedEvent class and its listener
The event holds the data payload; the listener implements ShouldQueue so it runs asynchronously without blocking the checkout response.
// app/Events/OrderPlaced.php
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(public readonly Order $order) {}
}
// app/Listeners/SendInvoiceEmail.php
class SendInvoiceEmail implements ShouldQueue
{
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->user)->send(new InvoiceMail($event->order));
}
}Dispatch, register, and fake events
Event::fake() prevents real listeners from firing in tests; assertDispatched() verifies the event was dispatched with the expected payload.
// Dispatch the event
OrderPlaced::dispatch($order);
// Register in EventServiceProvider
protected $listen = [
OrderPlaced::class => [
SendInvoiceEmail::class,
UpdateInventory::class,
NotifyWarehouse::class,
],
];
// In a test
Event::fake();
$this->post('/checkout', $payload);
Event::assertDispatched(OrderPlaced::class, fn($e) => $e->order->id === $orderId);
Discussion