Events, listeners and observers

Laravel · Advanced · 12 min

Events keep controllers focused while letting features like notifications, audit logs and analytics plug in.

An event is a plain class describing something that happened, such as OrderShipped. Listeners react to it. This decouples the code that causes an action from everything that should follow: the shipping code doesn't need to know about emails, analytics or webhooks.

Create them with php artisan make:event OrderShipped and php artisan make:listener SendShipmentEmail --event=OrderShipped. Laravel discovers listeners automatically: any listener in app/Listeners whose handle method type-hints an event is registered for it. You can also register explicitly with Event::listen() in a service provider.

Fire an event with OrderShipped::dispatch($order) or event(new OrderShipped($order)).

  • A listener that implements ShouldQueue runs on the queue, so slow side effects don't delay the response. It supports the same options as jobs (tries, backoff, queue name).
  • Implement ShouldDispatchAfterCommit on the event (or ShouldQueueAfterCommit on a listener) so nothing reacts to data that might be rolled back.

Model events fire automatically: creating, created, updating, updated, saving, saved, deleting, deleted and more. Group handlers in an observer (php artisan make:observer PostObserver --model=Post) and attach it with the #[ObservedBy(PostObserver::class)] attribute on the model.

Be careful: model events don't fire for mass updates like Post::where(...)->update([...]), because no models are loaded. Overusing events makes control flow hard to follow, so keep them for genuine side effects.

Example

<?php
class OrderShipped implements ShouldDispatchAfterCommit
{
    use Dispatchable, SerializesModels;
    public function __construct(public Order $order) {}
}

// Listener: auto-discovered from the handle() type-hint
class SendShipmentEmail implements ShouldQueue
{
    public function handle(OrderShipped $e): void { Mail::to($e->order->customer)->send(new ShipmentMail($e->order)); }
}
#[ObservedBy(OrderObserver::class)]
class Order extends Model {}
class OrderObserver
{
    public function updated(Order $order): void
    {
        if ($order->wasChanged('status')) AuditLog::record($order, 'status');   // audit trail
    }
}
DB::transaction(function () use ($order) {
    $order->update(['status' => 'shipped']);
    OrderShipped::dispatch($order);   // delivered only after commit
});

Remember

  • Events describe what happened; listeners decide what follows
  • Queued listeners keep side effects off the request path
  • Observers handle model lifecycle events, but mass updates skip them

Watch out for

  • Relying on an observer's updated hook while using query-builder mass updates, which fire no model events.

Check yourself

  1. Does Post::where('draft', true)->update(['draft' => false]) trigger PostObserver::updated?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/events

Builds on

Unlocks

Nothing builds on this yet.