Laravel Events, Listeners & Observers: Step-by-Step Guide

·

·

Laravel events let you broadcast that something happened in your application and react with listeners without tight coupling. Combined with Eloquent observers, you can keep controllers thin while still running side effects like emails, analytics, and audit logs.

Creating Events and Listeners

Events represent domain actions such as OrderPlaced, UserRegistered, or InvoicePaid. Listeners handle those events asynchronously or synchronously depending on how you register them.

Artisan generators create the boilerplate so you focus on payload design and handler logic instead of wiring classes by hand.

Step 1: Generate an event class with Artisan

Run php artisan make:event OrderPlaced to scaffold a class in app/Events. Add public properties or constructor arguments for the data listeners need, such as the Order model instance.

Serializable models use SerializesModels so queued listeners can safely rebuild Eloquent instances after unserialization without stale connection issues.

php artisan make:event OrderPlaced

// app/Events/OrderPlaced.php
public function __construct(public Order $order) {}

Step 2: Create a listener that reacts to the event

Use php artisan make:listener SendOrderConfirmation –event=OrderPlaced. The handle method receives the event object and performs one focused responsibility.

Keep listeners small. If a listener grows complex, extract a dedicated action or service class and call it from handle so testing stays straightforward.

php artisan make:listener SendOrderConfirmation --event=OrderPlaced

Step 3: Register mappings in EventServiceProvider

Open app/Providers/EventServiceProvider.php and map OrderPlaced::class to SendOrderConfirmation::class inside the $listen array.

Laravel auto-discovers listeners in modern apps, but explicit registration documents intent and prevents duplicate handlers during refactors.

protected $listen = [
    OrderPlaced::class => [
        SendOrderConfirmation::class,
    ],
];

Step 4: Dispatch events from services or controllers

After persisting an order, call event(new OrderPlaced($order)) or OrderPlaced::dispatch($order) if the event uses Dispatchable.

Dispatch from the domain layer when possible so every code path that creates an order triggers the same side effects without copy-pasting listener calls.

event(new OrderPlaced($order));
// or
OrderPlaced::dispatch($order);

Queued Listeners and Subscribers

Heavy listeners—email, webhooks, PDF generation—should run on the queue so HTTP requests stay fast.

Event subscribers group many listener mappings in one class, which keeps EventServiceProvider readable on large applications.

Step 1: Implement ShouldQueue on listeners

Add implements ShouldQueue to your listener class. Laravel serializes the event and pushes the job to your configured queue connection.

Set public $queue = 'notifications' or call $this->onQueue('notifications') in a listener constructor to route jobs to dedicated workers.

class SendOrderConfirmation implements ShouldQueue
{
    use InteractsWithQueue;
}

Step 2: Configure queue workers in development

Run php artisan queue:work –tries=3 while testing queued listeners locally. Without a worker, jobs sit in the database or Redis queue unprocessed.

Use php artisan queue:failed to inspect failures and queue:retry for transient errors like SMTP timeouts.

php artisan queue:work --tries=3
php artisan queue:failed

Step 3: Create an event subscriber class

php artisan make:listener OrderEventSubscriber –event=OrderPlaced generates a subscriber skeleton. Implement subscribe() returning an array of event => method pairs.

Register the subscriber in EventServiceProvider::boot with Event::subscribe(OrderEventSubscriber::class).

public function subscribe(Dispatcher $events): array
{
    return [
        OrderPlaced::class => 'onOrderPlaced',
    ];
}

Step 4: Test listeners with Event::fake

In PHPUnit or Pest, call Event::fake([OrderPlaced::class]) before exercising the action under test.

Assert Event::assertDispatched(OrderPlaced::class) or assertListening to verify wiring without sending real emails or hitting external APIs.

Event::fake();
// act...
Event::assertDispatched(OrderPlaced::class);

Eloquent Observers and Model Hooks

Observers attach to a single Eloquent model and respond to creating, updating, deleting, and restoring lifecycle events.

They are ideal for maintaining computed fields, clearing caches, or writing audit rows whenever model state changes.

Step 1: Generate an observer with Artisan

Run php artisan make:observer OrderObserver –model=Order. Laravel creates methods like created, updated, and deleted you can implement incrementally.

Register the observer in AppServiceProvider::boot using Order::observe(OrderObserver::class) or the #[ObservedBy] attribute on the model.

php artisan make:observer OrderObserver --model=Order

Step 2: Implement created and updated hooks

Inside created(Order $order), assign an order number or default status before the row is committed if you used the creating hook instead.

Use updated when you need to react only after successful persistence, such as syncing search indexes when specific columns change.

public function created(Order $order): void
{
    AuditLog::record('order.created', $order);
}

Step 3: Avoid infinite loops in observers

Calling $model->save() inside updated triggers another updated event. Use saveQuietly(), withoutEvents(), or compare dirty attributes before saving again.

Extract shared work to queued jobs dispatched from the observer so model saves stay fast during bulk imports.

  • Use saveQuietly() for silent updates
  • Compare isDirty() before re-saving

Step 4: Choose events vs observers deliberately

Use domain events when multiple bounded contexts care about the same business moment. Use observers when logic is tightly coupled to one model lifecycle.

Document the choice in your team wiki so new developers do not duplicate logic in both places.

Aadyanex builds production-ready Laravel applications with clean architecture, secure APIs, and long-term support.