Laravel Events & Observers: Production Patterns
Laravel Events & Observers: Decoupled Application Architecture
Reading Time: 25 minutes
Introduction
Events and Observers are Laravel's primary tools for decoupling application logic. They allow you to react to发生的事情 without coupling the triggering code to the reaction.
An Event is a recorded occurrence — "a user registered", "an order was placed", "a payment failed". A Listener is the code that responds to that event. An Observer is a listener specialized for Eloquent model events.
This separation lets you add features like sending emails, logging activity, updating caches, or notifying third-party services — without modifying the original code that triggered the event.
The Problem
Without events, adding cross-cutting concerns leads to tightly coupled, hard-to-maintain code:
class AuthController extends Controller
{
public function register(RegisterRequest $request): JsonResponse
{
$user = User::create($request->validated());
// Cross-cutting concerns mixed with registration logic
Mail::to($user)->send(new WelcomeEmail($user));
Log::info('User registered', ['id' => $user->id]);
$this->analytics->track('user_registered', $user);
$user->notify(new VerifyEmail);
return response()->json($user, 201);
}
}
Every new requirement — send a Slack notification, create a default workspace, sync to CRM — requires modifying AuthController. This breaks the Open/Closed Principle.
Events and Listeners
graph LR
A[UserRegistered Event] --> B[SendWelcomeEmail]
A --> C[LogRegistration]
A --> D[SyncToCRM]
A --> E[CreateDefaultWorkspace]
A --> F[NotifyAdmins]
G[Register Code] -- dispatches --> A
Creating Events and Listeners
# Create both event and listener
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered
# Or generate the whole pair at once
php artisan event:generate
Defining an Event
class UserRegistered
{
use Dispatchable, InteractsWithSockets;
public function __construct(
public User $user,
) {}
}
Defining a Listener
class SendWelcomeEmail
{
public function __construct(
private Mailer $mailer,
) {}
public function handle(UserRegistered $event): void
{
$this->mailer->send(
$event->user->email,
new WelcomeEmail($event->user)
);
}
}
class LogRegistration
{
public function handle(UserRegistered $event): void
{
Log::info('User registered', [
'id' => $event->user->id,
'email' => $event->user->email,
'time' => now(),
]);
}
}
Registering Events and Listeners
Laravel 11+ (bootstrap/app.php):
return Application::configure(basePath: dirname(__DIR__))
->withEvents()
->withRouting(...)
->create();
Use the EventServiceProvider pattern or the boot method:
// AppServiceProvider or EventServiceProvider
Event::listen(
UserRegistered::class,
SendWelcomeEmail::class,
);
Event::listen(
UserRegistered::class,
LogRegistration::class,
);
Dispatching Events
// Using the event helper
event(new UserRegistered($user));
// Using the dispatch method on the event
UserRegistered::dispatch($user);
// Using the Event facade
Event::dispatch(new UserRegistered($user));
The controller becomes clean:
class AuthController extends Controller
{
public function register(RegisterRequest $request): JsonResponse
{
$user = User::create($request->validated());
UserRegistered::dispatch($user);
return response()->json($user, 201);
}
}
Event Subscribers
Subscribers are classes that define multiple event listeners internally:
class UserEventSubscriber
{
public function handleUserRegistered(UserRegistered $event): void
{
// Handle registration
}
public function handleUserDeleted(UserDeleted $event): void
{
// Handle deletion
}
public function handlePasswordReset(PasswordReset $event): void
{
// Handle password reset
}
public function subscribe(Dispatcher $events): array
{
return [
UserRegistered::class => 'handleUserRegistered',
UserDeleted::class => 'handleUserDeleted',
PasswordReset::class => 'handlePasswordReset',
];
}
}
Registration:
Event::subscribe(UserEventSubscriber::class);
Queued Listeners
Slow listeners should be queued to avoid delaying the response:
class SendWelcomeEmail implements ShouldQueue, ShouldBeUnique
{
public string $queue = 'emails';
public int $delay = 10; // Delay in seconds
public int $tries = 3;
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
// Unique job — only one WelcomeEmail per user
public function uniqueId(): string
{
return $event->user->id;
}
// Retry delay
public function backoff(): array
{
return [5, 10, 30];
}
// On failure
public function failed(UserRegistered $event, Throwable $e): void
{
Log::error('Welcome email failed', [
'user' => $event->user->id,
'error' => $e->getMessage(),
]);
}
}
Conditional Queueing
// Always on queue
Event::listen(
UserRegistered::class,
SendWelcomeEmail::class,
);
// Conditionally (in listener constructor or middleware)
class SendWelcomeEmail
{
public bool $queue = true; // Always queue
// Or, use ShouldQueue interface on the listener class itself
}
Model Events
Eloquent fires events automatically during model lifecycle:
| Event | Triggered When |
|---|---|
retrieved | After a model is fetched from DB |
creating | Before a model is saved for the first time |
created | After a model is saved for the first time |
updating | Before an existing model is saved |
updated | After an existing model is saved |
saving | Before a model is created OR updated |
saved | After a model is created OR updated |
deleting | Before a model is deleted |
deleted | After a model is deleted |
trashed | Before a model is soft-deleted |
forceDeleted | After a model is force-deleted |
restoring | Before a soft-deleted model is restored |
restored | After a soft-deleted model is restored |
replicating | Before a model is replicated |
Using Model Events in the Boot Method
class User extends Model
{
protected static function booted(): void
{
static::creating(function (User $user) {
// Set default values before creation
$user->uuid ??= (string) Str::uuid();
});
static::created(function (User $user) {
// Create default profile
$user->profile()->create([
'avatar' => 'default.png',
]);
});
static::updated(function (User $user) {
// Log profile changes
Log::info('User updated', ['id' => $user->id]);
});
static::deleted(function (User $user) {
// Clean up related data
$user->posts()->delete();
$user->comments()->delete();
});
}
}
Observers
Observers group all model event listeners into a single class:
php artisan make:observer UserObserver --model=User
class UserObserver
{
public function creating(User $user): void
{
$user->uuid ??= (string) Str::uuid();
}
public function created(User $user): void
{
$user->profile()->create(['avatar' => 'default.png']);
Log::info('User created', ['id' => $user->id, 'email' => $user->email]);
}
public function updating(User $user): void
{
// Track changes
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}
public function updated(User $user): void
{
if ($user->wasChanged('email')) {
Mail::to($user)->send(new EmailChangedNotification($user));
}
}
public function deleting(User $user): void
{
// Prevent deletion of super admin
if ($user->isSuperAdmin()) {
return false; // Cancel the delete
}
}
public function deleted(User $user): void
{
Log::info('User deleted', ['id' => $user->id]);
// Queue cleanup jobs
CleanupUserData::dispatch($user->id)->onQueue('cleanup');
}
public function restored(User $user): void
{
Log::info('User restored', ['id' => $user->id]);
}
public function forceDeleted(User $user): void
{
Log::info('User force deleted', ['id' => $user->id]);
// Permanently remove external references
ExternalAPIService::removeUser($user->id);
}
}
Registering Observers
// AppServiceProvider or EventServiceProvider
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
User::observe(UserObserver::class);
}
}
// Or use Laravel 11+ automatic discovery
// Observers in app/Observers/ are auto-registered
Observer Auto-Discovery (Laravel 11+)
Laravel 11 automatically finds observers in app/Observers/ and registers them for their corresponding model. The convention is ModelNameObserver in app/Observers/.
Events vs Observers
| Aspect | Events | Observers |
|---|---|---|
| Scope | Application-wide | Model-specific |
| Trigger | Explicit event() call | Automatic Eloquent lifecycle |
| Use case | Business logic consequences | Data integrity, cross-cutting model concerns |
| Dependencies | Can inject services | Can inject services |
| Queuing | Via ShouldQueue | Via ShouldQueue (attached to the underlying event) |
When to Use Which
Use Observers for:
- Data integrity — UUID generation, default values, cascading deletes
- Audit logging — track model changes
- Automatic relationship management
Use Events for:
- Business process coordination — "order was placed" triggers email, inventory, analytics
- Cross-cutting features — notifications, webhooks
- Workflows that span multiple models
Queued Model Events
Observers and model events can dispatch queued jobs:
class UserObserver
{
public function created(User $user): void
{
// This runs synchronously during the request
ProcessUserAvatar::dispatch($user)->onQueue('images');
}
}
Event Discovery
Laravel can auto-discover events and listeners. In EventServiceProvider:
public function shouldDiscoverEvents(): bool
{
return true;
}
By default, events are discovered by scanning the Listeners directory. Customize:
protected function configureDiscoverEvents(): void
{
$this->discoverEvents(function () {
return [
app_path('Listeners'),
app_path('Domain/*/Listeners'),
];
});
}
Common Mistakes
Mistake 1: Side Effects in creating vs created
// ❌ Using `creating` for operations that need the model's ID
static::creating(function (User $user) {
$user->profile()->create([]); // Fails — user has no ID yet
});
// ✅ Use `created` instead
static::created(function (User $user) {
$user->profile()->create([]); // Works — user has ID
});
Mistake 2: Returning Values from the Wrong Hook
// ✅ `creating` and `saving` can return false to halt
static::creating(function (User $user) {
if (User::where('email', $user->email)->exists()) {
return false; // Prevents save
}
});
// ❌ `created` and `saved` cannot halt — model is already saved
static::created(function (User $user) {
return false; // No effect — model is already in DB
});
Mistake 3: Cascading Events in a Loop
// ❌ Each iteration fires created/saved events
foreach ($emails as $email) {
$user = User::create(['email' => $email]);
// Queue jobs fire N times
}
// ✅ Use `query() builder` to insert without events
User::withoutEvents(function () use ($emails) {
foreach ($emails as $email) {
User::create(['email' => $email]);
}
});
// Then dispatch a single batch event
UserBatchCreated::dispatch(count($emails));
Mistake 4: Heavy Operations in Synchronous Listeners
// ❌ Blocks the response
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
Mail::send(...); // Takes 500ms
Slack::notify(...); // Takes 200ms
SyncToCRM::sync($event->user); // Takes 1s
}
}
// ✅ Queue the listener
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// All work happens in the queue worker
}
}
Mistake 5: Deadly Observer-Events Loop
class OrderObserver
{
public function updated(Order $order): void
{
if ($order->wasChanged('status')) {
OrderStatusChanged::dispatch($order);
}
}
}
// Listener updates the order again, triggering the observer again
class OrderStatusChanged
{
public function handle(OrderStatusChanged $event): void
{
$event->order->update(['processed_at' => now()]);
// This fires OrderObserver::updated again... infinite loop!
}
}
Fix: Guard against recursion:
class OrderObserver
{
public function updated(Order $order): void
{
if ($order->wasChanged('status') && !$order->wasChanged('processed_at')) {
OrderStatusChanged::dispatch($order);
}
}
}
Best Practices Summary
- Use events for business workflows — decouple the trigger from the reaction
- Use observers for model integrity — UUIDs, defaults, cascades
- Queue slow listeners — any listener doing I/O (email, API calls, file processing)
- Use
withoutEventsfor bulk operations — avoid N events for N records - Guard against infinite loops — check
wasChangedbefore dispatching - Use event subscribers for related events — groups User events, Order events, etc.
- Register observers in service providers — not in models'
bootedfor Observers - Use
ShouldBeUniquefor idempotent listeners — prevent duplicate processing
Interview Questions
- What is the difference between an Event and an Observer?
- How do you make an event listener run on the queue?
- What is the difference between
creatingandcreatedmodel events? - How do you prevent an Eloquent model from being saved from an observer?
- Explain the
withoutEventsmethod and when you would use it. - How do event subscribers differ from regular listeners?
- How can you prevent infinite loops between observers and events?