Understanding Laravel's Event System: Events, Listeners, and Observers
One of the clearest signals that a Laravel codebase is growing up is when developers stop stuffing everything into controllers and start using events. The event system is Laravel's built-in mechanism for decoupling application logic — separating what happened from what should happen because of it. This lesson explains how it works, why it matters, and how to choose between events, observers, and the various listener patterns.
The Core Concept: Decoupling What Happened From What Follows
Imagine a user registers on your platform. After registration, your app needs to send a welcome email, create a trial subscription, notify the sales team, and fire a marketing pixel. The naive approach puts all of that inside UserController::store(). It works, but now your controller knows about email, billing, Slack, and analytics. Every new requirement means editing the same method.
The event-driven approach instead fires a single event — UserRegistered — and lets any number of listeners react to it independently. The controller knows one thing: a user registered. Everything that follows lives elsewhere.
Events — Plain PHP Classes Carrying Data
An event is a simple PHP class. Generate one with Artisan:
php artisan make:event UserRegistered
class UserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public User $user) {}
}
The event carries the data that listeners will need — in this case, the User model. Nothing else. Fire it from wherever the thing actually happened:
class UserController extends Controller
{
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->validated());
UserRegistered::dispatch($user);
return redirect()->route('dashboard');
}
}
The controller's job is done. It does not know what happens next.
Listeners — Single-Responsibility Reactions
A listener is a class with a handle() method that receives the event:
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener CreateTrialSubscription --event=UserRegistered
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
}
Register listeners in EventServiceProvider (or, in Laravel 11+, they are auto-discovered if they follow naming conventions):
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
CreateTrialSubscription::class,
NotifySalesTeam::class,
],
];
When UserRegistered is dispatched, Laravel calls each listener's handle() method in order. Adding a new behavior on registration means adding a new listener class — no existing code changes.
Queued Listeners — Move Slow Work Off the Request
If a listener does something slow (sending email, calling an external API, generating a PDF), you want it to run in a background job, not during the HTTP request. Implement ShouldQueue and it happens automatically:
class SendWelcomeEmail implements ShouldQueue
{
public string $queue = 'notifications';
public int $delay = 10; // seconds
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
}
The listener is dispatched as a queued job. The user gets their redirect response immediately while the email goes out in the background. Failed jobs get the standard queue retry behavior.
Event Subscribers — Group Related Listeners Together
An event subscriber is a single class that handles multiple events. This is useful when a set of listeners are conceptually related and you want them in one place:
php artisan make:listener UserEventSubscriber --event=UserRegistered
class UserEventSubscriber
{
public function handleRegistration(UserRegistered $event): void
{
// ...
}
public function handleProfileUpdated(UserProfileUpdated $event): void
{
// ...
}
public function subscribe(Dispatcher $events): array
{
return [
UserRegistered::class => 'handleRegistration',
UserProfileUpdated::class => 'handleProfileUpdated',
];
}
}
Register the subscriber in EventServiceProvider:
protected $subscribe = [
UserEventSubscriber::class,
];
Observers — When You Want to React to Eloquent Lifecycle Events
Observers are a shortcut specifically for reacting to Eloquent model events: creating, created, updating, updated, deleting, deleted, restored, and forceDeleted.
php artisan make:observer OrderObserver --model=Order
class OrderObserver
{
public function created(Order $order): void
{
$order->generateReferenceNumber();
}
public function updated(Order $order): void
{
if ($order->wasChanged('status')) {
OrderStatusChanged::dispatch($order);
}
}
public function deleting(Order $order): void
{
if ($order->status === OrderStatus::Processing) {
throw new \Exception('Cannot delete an order that is being processed.');
}
}
}
Register it in a service provider:
Order::observe(OrderObserver::class);
Note the difference: observers react to Eloquent's internal model lifecycle. Custom events are fired explicitly by your application code. Both are valid — use observers for tightly coupled model behavior (generating reference numbers, enforcing state rules) and custom events for application-level domain events (user registered, payment received).
When to Use Events vs Observers vs Direct Calls
The right tool depends on the coupling level:
Use a direct method call when the reaction is the controller's responsibility and there is only one of it. Not everything needs to be decoupled.
Use an observer when you want to react to Eloquent model lifecycle events (created, updated, deleted) with logic that is genuinely about the model — not about what triggered the change.
Use events and listeners when multiple unrelated parts of your application need to react to something, when reactions should be queued, or when you want to add behaviors without modifying existing code.
Events are one of those features that feel like overhead when your app is small, and indispensable once it grows. The time to add them is before your controllers become impossible to read, not after.