Introduction
Events are Laravel's implementation of the observer pattern — a way to fire a named signal ("OrderPlaced") and have any number of listeners react to it, without the code that fires the event knowing which listeners exist. They're the decoupling tool you reach for when one action needs to trigger many side effects.
Key Concepts
- Event: A plain PHP class that represents something that happened in the domain.
- Listener: A class with a
handle()method that runs when a specific event fires. - Dispatcher: The service that routes events to their listeners.
- Queued listener: A listener implementing
ShouldQueuethat runs in the background instead of inline.
Real World Context
Without events, an OrderController@store might call sendConfirmationEmail(), updateInventory(), notifyWarehouse(), trackInAnalytics(), and syncToCRM() directly — five concerns tangled in one controller. Fire an OrderPlaced event instead, and each side effect becomes a small listener class that you can test, queue, or disable independently.
Deep Dive
Why Events?
Without Events: With Events:
┌───────────────────────┐ ┌───────────────────────┐
│ OrderController │ │ OrderController │
│ │ │ │
│ - Process payment │ │ - Process payment │
│ - Send confirmation │ │ - Dispatch OrderPlaced│
│ - Update inventory │ └───────────────────────┘
│ - Notify warehouse │ │
│ - Add to analytics │ ▼
│ - Send to CRM │ ┌───────────────────────┐
│ (tightly coupled!) │ │ Event: OrderPlaced │
└───────────────────────┘ └───────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│SendEmailJob │ │UpdateStock │ │Analytics │
└─────────────┘ └─────────────┘ └─────────────┘
Creating Events
bashphp artisan make:event OrderPlaced
This creates app/Events/OrderPlaced.php:
php<?php namespace App\Events; use App\Models\Order; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class OrderPlaced { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct( public Order $order ) {} }
Creating Listeners
bashphp artisan make:listener SendOrderConfirmation --event=OrderPlaced
This creates app/Listeners/SendOrderConfirmation.php:
php<?php namespace App\Listeners; use App\Events\OrderPlaced; use App\Notifications\OrderConfirmation; class SendOrderConfirmation { public function handle(OrderPlaced $event): void { $event->order->user->notify( new OrderConfirmation($event->order) ); } }
Registering Events & Listeners
In AppServiceProvider:
phpuse App\Events\OrderPlaced; use App\Listeners\SendOrderConfirmation; use App\Listeners\UpdateInventory; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen( OrderPlaced::class, SendOrderConfirmation::class ); Event::listen( OrderPlaced::class, UpdateInventory::class ); }
Using Closures
phpEvent::listen(function (OrderPlaced $event) { Log::info('Order placed', ['order_id' => $event->order->id]); });
Dispatching Events
phpuse App\Events\OrderPlaced; class OrderController extends Controller { public function store(Request $request) { $order = Order::create($request->validated()); // Dispatch the event OrderPlaced::dispatch($order); // Or using event() helper event(new OrderPlaced($order)); return redirect()->route('orders.show', $order); } }
Queued Listeners
Process listeners in the background:
php<?php namespace App\Listeners; use App\Events\OrderPlaced; use Illuminate\Contracts\Queue\ShouldQueue; class UpdateInventory implements ShouldQueue { public function handle(OrderPlaced $event): void { foreach ($event->order->items as $item) { $item->product->decrement('stock', $item->quantity); } } }
Queued Listener Configuration
phpclass UpdateInventory implements ShouldQueue { public $connection = 'redis'; public $queue = 'inventory'; public $delay = 10; // seconds public function viaQueue(): string { return 'inventory'; } public function shouldQueue(OrderPlaced $event): bool { return $event->order->total > 100; } }
Event Subscribers
Group related listeners:
php<?php namespace App\Listeners; use App\Events\OrderPlaced; use App\Events\OrderShipped; use App\Events\OrderCancelled; use Illuminate\Events\Dispatcher; class OrderEventSubscriber { public function handleOrderPlaced(OrderPlaced $event): void { // Handle order placed } public function handleOrderShipped(OrderShipped $event): void { // Handle order shipped } public function subscribe(Dispatcher $events): void { $events->listen( OrderPlaced::class, [self::class, 'handleOrderPlaced'] ); $events->listen( OrderShipped::class, [self::class, 'handleOrderShipped'] ); } }
Register in AppServiceProvider:
phpEvent::subscribe(OrderEventSubscriber::class);
Common Pitfalls
- Firing events inside database transactions — a queued listener may start processing before the transaction commits, so the listener can't see the rows the event was about. Use
ShouldDispatchAfterCommiton the event, or dispatch it afterDB::commit()returns. - Putting heavy work in a synchronous listener — an inline listener that takes 2 seconds blocks the HTTP response just like inline logic would. Mark the listener
ShouldQueueso it runs in the background.
Best Practices
- Name events in past tense (
OrderPlaced, notPlaceOrder) — events describe something that already happened. Imperative names belong on jobs or commands. - Keep listeners single-purpose — one listener per side effect makes each one easy to test and disable. Avoid "do-everything" listeners that pile five actions into one
handle().
Summary
- Events decouple the action that triggers side effects from the side effects themselves.
- Listeners live in
app/Listenersand are auto-discovered by type hint. - Mark a listener
ShouldQueueto run it in the background. - Subscribers group related listeners in one class with a
subscribe()method.