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 ShouldQueue that 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

bash
php 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

bash
php 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:

php
use 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

php
Event::listen(function (OrderPlaced $event) {
    Log::info('Order placed', ['order_id' => $event->order->id]);
});

Dispatching Events

php
use 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

php
class 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:

php
Event::subscribe(OrderEventSubscriber::class);

Common Pitfalls

  1. 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 ShouldDispatchAfterCommit on the event, or dispatch it after DB::commit() returns.
  2. 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 ShouldQueue so it runs in the background.

Best Practices

  1. Name events in past tense (OrderPlaced, not PlaceOrder) — events describe something that already happened. Imperative names belong on jobs or commands.
  2. 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/Listeners and are auto-discovered by type hint.
  • Mark a listener ShouldQueue to run it in the background.
  • Subscribers group related listeners in one class with a subscribe() method.
✓ Completed