Backend

Laravel Events and Listeners

Mayur Dabhi
Mayur Dabhi
April 11, 2026
14 min read

One of the most powerful patterns in modern application architecture is the Observer pattern — and Laravel's Events and Listeners system is a first-class implementation of it. Instead of cramming every consequence of an action into a single controller method, you fire an event and let dedicated listener classes react to it. The result is leaner controllers, highly testable code, and an architecture that scales gracefully as business requirements grow.

In this guide you'll learn exactly how Laravel's event system works under the hood, how to create and dispatch events, write synchronous and queued listeners, and apply the pattern to real scenarios you'll encounter in production apps.

Why Events and Listeners?

Every time a user registers on your platform you might need to: send a welcome email, create a default profile record, notify an admin, log the action to an analytics service, and provision a free trial. Without events, all of this logic lives in one controller method. With events, each concern lives in its own listener — independently testable, individually toggleable, and trivially extensible.

How Laravel's Event System Works

At its core, Laravel's event system is a publish/subscribe bus. Your application code dispatches (publishes) an event, and zero or more listeners (subscribers) react to it. The mapping between events and listeners is registered in the EventServiceProvider.

Under the hood, Laravel uses its service container to resolve listener classes, meaning listeners can type-hint any dependency and have it injected automatically — the same way controllers and jobs work.

Controller dispatches event event() UserRegistered Event class Laravel Event Bus SendWelcomeEmail CreateUserProfile NotifyAdminSlack Laravel Event Dispatch Flow One event dispatched → multiple independent listeners react

A single dispatched event fans out to all registered listeners

Creating Events and Listeners

Artisan makes scaffolding events and listeners trivial. The convention is to name events in the past tense (something that happened) and listeners as actions (something that should happen next).

1

Generate the Event class

Run php artisan make:event UserRegistered to create app/Events/UserRegistered.php.

2

Generate the Listener class

Run php artisan make:listener SendWelcomeEmail --event=UserRegistered to create app/Listeners/SendWelcomeEmail.php pre-typed to your event.

3

Register in EventServiceProvider

Map the event to its listeners inside the $listen array of app/Providers/EventServiceProvider.php.

4

Dispatch the event

Call event(new UserRegistered($user)) or UserRegistered::dispatch($user) from anywhere in your application.

The Event Class

app/Events/UserRegistered.php
<?php

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * The newly registered user.
     */
    public User $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}
SerializesModels Trait

The SerializesModels trait ensures that when an event is queued, Eloquent models are stored by their primary key rather than the full object. When the queued job runs, Laravel automatically re-fetches the fresh model from the database — preventing stale data issues.

The Listener Class

app/Listeners/SendWelcomeEmail.php
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    /**
     * Handle the event.
     */
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user->email)
            ->send(new WelcomeEmail($event->user));
    }
}

Registering Events in EventServiceProvider

app/Providers/EventServiceProvider.php
<?php

namespace App\Providers;

use App\Events\UserRegistered;
use App\Events\OrderPlaced;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\CreateUserProfile;
use App\Listeners\NotifyAdminSlack;
use App\Listeners\SendOrderConfirmation;
use App\Listeners\UpdateInventory;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        UserRegistered::class => [
            SendWelcomeEmail::class,
            CreateUserProfile::class,
            NotifyAdminSlack::class,
        ],

        OrderPlaced::class => [
            SendOrderConfirmation::class,
            UpdateInventory::class,
        ],
    ];
}

Queued Listeners for Heavy Work

Sending emails, calling external APIs, generating reports — these are slow. You don't want a user waiting for your registration controller to finish all of that before they get a response. Queued listeners run the heavy work asynchronously in the background using Laravel's queue system.

To make a listener queued, simply implement the ShouldQueue interface. That's all — Laravel does the rest automatically.

app/Listeners/SendWelcomeEmail.php — Queued Version
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The queue this listener should run on.
     */
    public string $queue = 'emails';

    /**
     * Delay in seconds before processing.
     */
    public int $delay = 30;

    /**
     * Number of times to retry on failure.
     */
    public int $tries = 3;

    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user->email)
            ->send(new WelcomeEmail($event->user));
    }

    /**
     * Handle a job failure.
     */
    public function failed(UserRegistered $event, \Throwable $exception): void
    {
        // Log failure, notify dev team, etc.
        \Log::error("Failed to send welcome email to {$event->user->email}: {$exception->getMessage()}");
    }
}
Queue Worker Required

Queued listeners only run if you have a queue worker processing jobs. In development run php artisan queue:work. In production, use Supervisor to keep the worker running persistently, or Laravel Horizon for Redis-backed queues with a beautiful dashboard.

Conditionally Queueing a Listener

Sometimes you want a listener to be synchronous for local requests but queued in production, or queued only under certain conditions. Use the shouldQueue method:

Conditional Queueing
class SendWelcomeEmail implements ShouldQueue
{
    /**
     * Determine whether the listener should be queued.
     */
    public function shouldQueue(UserRegistered $event): bool
    {
        // Only queue if the user has a verified email
        return $event->user->email_verified_at !== null;
    }
}

Dispatching Events

Laravel provides multiple syntaxes for dispatching events, all equivalent under the hood:

Dispatching Methods
use App\Events\UserRegistered;

// Method 1: Global helper (most common)
event(new UserRegistered($user));

// Method 2: Static dispatch on the event class (uses Dispatchable trait)
UserRegistered::dispatch($user);

// Method 3: Via the Event facade
use Illuminate\Support\Facades\Event;
Event::dispatch(new UserRegistered($user));

// Dispatch after database transaction commits
// Useful when the event payload references a model just created
UserRegistered::dispatchAfterResponse($user);
UserRegistered::dispatchIf($user->isActive(), $user);
UserRegistered::dispatchUnless($user->isBanned(), $user);

Real-World Controller Example

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Events\UserRegistered;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        $user = User::create([
            'name'     => $validated['name'],
            'email'    => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        // Fire the event — all consequences handled by listeners
        UserRegistered::dispatch($user);

        return redirect('/dashboard')->with('success', 'Welcome aboard!');
    }
}

Notice how clean the controller is. It does its one job — create a user and redirect — and the event handles every downstream concern. Adding new behaviour after registration (e.g., assign a referral bonus) requires only adding a new listener and registering it, with zero changes to the controller.

Event Subscribers

When you have many related listeners, grouping them into a single subscriber class keeps your EventServiceProvider tidy. A subscriber is a class that defines a subscribe method mapping events to its own methods.

app/Listeners/UserEventSubscriber.php
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserPasswordChanged;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
    public function handleUserRegistration(UserRegistered $event): void
    {
        // Send welcome email, create profile, etc.
    }

    public function handleUserLogin(UserLoggedIn $event): void
    {
        // Update last_login_at, log IP address
        $event->user->update(['last_login_at' => now()]);
    }

    public function handlePasswordChange(UserPasswordChanged $event): void
    {
        // Notify user via email, invalidate other sessions
    }

    /**
     * Register the listeners for the subscriber.
     */
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(UserRegistered::class,   [self::class, 'handleUserRegistration']);
        $events->listen(UserLoggedIn::class,      [self::class, 'handleUserLogin']);
        $events->listen(UserPasswordChanged::class, [self::class, 'handlePasswordChange']);
    }
}

// Register in EventServiceProvider:
// protected $subscribe = [UserEventSubscriber::class];

Model Events: Observers

Laravel Eloquent models fire their own lifecycle events automatically: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, and restored. The cleanest way to listen to model events is with an Observer class.

Generate and implement an Observer
# Generate observer
php artisan make:observer UserObserver --model=User
app/Observers/UserObserver.php
<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Support\Str;

class UserObserver
{
    /**
     * Handle the User "creating" event.
     * Fires BEFORE the model is saved to the database.
     */
    public function creating(User $user): void
    {
        // Auto-generate a public UUID for external use
        $user->uuid = Str::uuid();
    }

    /**
     * Handle the User "created" event.
     * Fires AFTER the model is saved.
     */
    public function created(User $user): void
    {
        // Create associated settings with defaults
        $user->settings()->create([
            'theme'            => 'light',
            'notifications'    => true,
            'emails_marketing' => false,
        ]);
    }

    /**
     * Handle the User "updating" event.
     */
    public function updating(User $user): void
    {
        // Track when the email was changed
        if ($user->isDirty('email')) {
            $user->email_verified_at = null;
        }
    }

    /**
     * Handle the User "deleting" event.
     */
    public function deleting(User $user): void
    {
        // Clean up related data before deletion
        $user->posts()->delete();
        $user->settings()->delete();
    }
}

// Register in AppServiceProvider::boot():
// User::observe(UserObserver::class);

Events vs Observers: When to Use Which

Scenario Use Events + Listeners Use Observers
Triggered by user actions (registration, checkout) ✅ Yes Possible but verbose
Reacting to model lifecycle (created, updated, deleted) Possible but extra boilerplate ✅ Yes — built for this
Multiple unrelated listeners ✅ Yes Not ideal
Auto-populate model fields before save Not ideal ✅ Yes (creating/saving hooks)
Broadcasting to WebSocket channels ✅ Yes (ShouldBroadcast) No
Queuing heavy work (emails, API calls) ✅ Yes (ShouldQueue on listener) Requires manual job dispatch

Broadcasting Events Over WebSockets

Laravel's event system integrates directly with Laravel Echo and Pusher (or Soketi/Reverb) to broadcast events to the browser in real time. Implement ShouldBroadcast on an event, define the broadcastOn method, and the event is automatically pushed to WebSocket clients when dispatched.

app/Events/OrderStatusUpdated.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderStatusUpdated implements ShouldBroadcast
{
    use Dispatchable, SerializesModels;

    public Order $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Broadcast on the user's private channel so only they receive it.
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel("orders.{$this->order->user_id}"),
        ];
    }

    /**
     * Data sent to the client.
     */
    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'status'   => $this->order->status,
            'updated'  => $this->order->updated_at->toIso8601String(),
        ];
    }

    /**
     * Event name visible on the client side.
     */
    public function broadcastAs(): string
    {
        return 'order.updated';
    }
}

Laravel Echo — Listening in the Browser

resources/js/app.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    forceTLS: true,
});

// Listen on the private channel for the logged-in user
Echo.private(`orders.${userId}`)
    .listen('.order.updated', (data) => {
        console.log('Order status changed:', data.status);
        showToast(`Your order #${data.order_id} is now ${data.status}`);
    });

Testing Events and Listeners

Testing with real events can cause side-effects (actual emails sent, real API calls made). Laravel's Event::fake() prevents events from being dispatched while letting you assert they were fired — perfect for controller tests.

tests/Feature/RegistrationTest.php
<?php

namespace Tests\Feature;

use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\CreateUserProfile;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class RegistrationTest extends TestCase
{
    public function test_registration_fires_user_registered_event(): void
    {
        Event::fake();

        $response = $this->post('/register', [
            'name'                  => 'Jane Doe',
            'email'                 => 'jane@example.com',
            'password'              => 'secret1234',
            'password_confirmation' => 'secret1234',
        ]);

        $response->assertRedirect('/dashboard');

        // Assert the event was dispatched
        Event::assertDispatched(UserRegistered::class, function ($event) {
            return $event->user->email === 'jane@example.com';
        });
    }

    public function test_correct_listeners_are_registered_for_event(): void
    {
        Event::fake();

        Event::assertListening(UserRegistered::class, SendWelcomeEmail::class);
        Event::assertListening(UserRegistered::class, CreateUserProfile::class);
    }
}

// Listener unit test — test in isolation
class SendWelcomeEmailTest extends TestCase
{
    public function test_sends_welcome_email(): void
    {
        Mail::fake();

        $user = User::factory()->create();
        $event = new UserRegistered($user);

        (new SendWelcomeEmail)->handle($event);

        Mail::assertSent(WelcomeEmail::class, fn ($mail) =>
            $mail->hasTo($user->email)
        );
    }
}
Fake Selectively

Pass specific event classes to Event::fake([UserRegistered::class]) to fake only those events while letting others fire normally. This is useful when some events trigger side effects that are part of what you're testing.

Best Practices and Key Takeaways

After working with Laravel's event system extensively, here are the patterns that consistently produce maintainable codebases:

Best Practices

  • Name events in the past tenseUserRegistered, OrderPlaced, PaymentFailed. They represent something that already happened.
  • Keep listeners focused — one listener, one job. SendWelcomeEmail sends email. Done. Don't bundle unrelated logic.
  • Queue I/O-bound listeners — anything touching email, HTTP APIs, or file systems should implement ShouldQueue.
  • Always implement failed() on queued listeners — log the failure and decide whether to retry or dead-letter the job.
  • Use dispatchAfterResponse() for events that must fire synchronously but shouldn't slow the HTTP response.
  • Prefer Observers for model lifecycle hooks — don't write manual event/listener pairs for creating/created when an Observer does it more cleanly.
  • Use Event::fake() in tests — never let tests send real emails or call real APIs.
"Events let you write code that describes what happened, not what to do about it. Every listener you add is a new piece of behaviour you can enable, disable, test, and deploy independently."

Laravel's event system is one of those features that doesn't feel necessary on a small project, but becomes indispensable as your application grows. Once your registration flow needs to do ten things instead of two, you'll be grateful every single one of those consequences lives in its own focused, testable listener class — not tangled together in a controller method. Start applying events and listeners today, and your future self will thank you.

Laravel Events Listeners Observer Pattern Queues PHP Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.