Laravel Events and Listeners
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.
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.
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).
Generate the Event class
Run php artisan make:event UserRegistered to create app/Events/UserRegistered.php.
Generate the Listener class
Run php artisan make:listener SendWelcomeEmail --event=UserRegistered to create app/Listeners/SendWelcomeEmail.php pre-typed to your event.
Register in EventServiceProvider
Map the event to its listeners inside the $listen array of app/Providers/EventServiceProvider.php.
Dispatch the event
Call event(new UserRegistered($user)) or UserRegistered::dispatch($user) from anywhere in your application.
The Event Class
<?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;
}
}
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
<?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
<?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.
<?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()}");
}
}
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:
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:
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
<?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.
<?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 observer
php artisan make:observer UserObserver --model=User
<?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.
<?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
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.
<?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)
);
}
}
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 tense —
UserRegistered,OrderPlaced,PaymentFailed. They represent something that already happened. - Keep listeners focused — one listener, one job.
SendWelcomeEmailsends 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/createdwhen 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.
