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. Implement ShouldQueue to 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

Example · php
<?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.

Example · bash
php artisan make:event OrderPlaced
php artisan make:listener SendInvoiceEmail --event=OrderPlaced

Event class and its listener

The event holds the data payload; the listener implements ShouldQueue so it runs asynchronously without blocking the checkout response.

Example · php
// 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.

Example · php
// 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

  • Be the first to comment on this lesson.