Laravel Observers: The Silent Watchers ð
Laravel Observers: The Silent Watchers ð
Ever felt like your Laravel controllers are doing too much? You're creating a user, sending a welcome email, creating a profile, logging the action, notifying admins, making coffee... okay maybe not the last one, but you get the point!
Let me tell you about Observers - Laravel's way of saying "I got this" while keeping your code clean.
What the Heck Is an Observer? ðĪ
Think of Observers as security cameras for your Eloquent models. They watch everything happening to your models and react automatically. Created a user? Observer knows. Updated a post? Observer's watching. Deleted something? Observer caught that too!
In production systems I've built at Cubet Techno Labs, Observers saved us from controller bloat and made our e-commerce backends way more maintainable.
The Problem: Controllers on Steroids ðŠâ
Here's what I see all the time (and what I used to do):
public function store(Request $request)
{
// Create user
$user = User::create($request->validated());
// Create profile
Profile::create(['user_id' => $user->id]);
// Send welcome email
Mail::to($user)->send(new WelcomeEmail($user));
// Log the registration
Log::info('New user registered', ['user_id' => $user->id]);
// Notify admins
Notification::send($admins, new NewUserNotification($user));
// Generate API token
$user->createToken('default');
return response()->json($user, 201);
}
This works, but YIKES! Your controller knows way too much. It's like hiring someone to make pizza who also has to grow the tomatoes, milk the cow for cheese, and deliver the pizza. Not scalable! ð
The Solution: Let Observers Watch ð
Create an observer:
php artisan make:observer UserObserver --model=User
Now move all that logic where it belongs:
class UserObserver
{
public function created(User $user)
{
// Auto-create profile
$user->profile()->create([
'bio' => 'New user - bio coming soon!',
]);
// Send welcome email
Mail::to($user)->queue(new WelcomeEmail($user));
// Generate default API token
$user->createToken('default');
Log::info('New user registered', ['user_id' => $user->id]);
}
public function updated(User $user)
{
// Track what changed
if ($user->isDirty('email')) {
Mail::to($user)->send(new EmailChangedNotification());
}
}
public function deleting(User $user)
{
// Clean up before deletion (note: deleting, not deleted!)
$user->posts()->delete();
$user->comments()->delete();
$user->sessions()->delete();
}
}
Register it in AppServiceProvider:
use App\Models\User;
use App\Observers\UserObserver;
public function boot()
{
User::observe(UserObserver::class);
}
Now your controller is CLEAN:
public function store(Request $request)
{
$user = User::create($request->validated());
return response()->json($user, 201);
}
Beautiful! One line! The Observer handles everything else automatically. ðŊ
Real Talk: When I Actually Use Observers ðž
As a Technical Lead, I've learned that Observers shine in these scenarios:
1. Model-Specific Side Effects When something ALWAYS needs to happen with a model, regardless of where it's created/updated. User always needs a profile? Observer it!
2. Automatic Cleanup Deleting a user should delete their data. Don't trust developers to remember - let the Observer handle it.
3. Audit Trails In a production e-commerce system I architected, we used Observers to track every price change, every inventory update. Gold for debugging customer complaints!
4. Computed Fields Need to auto-generate slugs? Calculate totals? Hash passwords (actually use a mutator for this, but you get the idea)?
Pro Tips From the Trenches ðïļ
Tip 1: Know Your Events
Observer methods you can use:
creating/created- Before/after insertupdating/updated- Before/after updatesaving/saved- Before/after create OR updatedeleting/deleted- Before/after deleteretrieved- When model is fetched (use sparingly!)restoring/restored- When soft-deleted model is restored
The "ing" vs "ed" trick:
- Use
creating/updating/deletingwhen you need to CHANGE the model before it's saved - Use
created/updated/deletedfor side effects AFTER it's saved
Tip 2: Don't Go Observer Crazy ðĪŠ
A pattern that saved us in a real project: If the logic is needed in EVERY context, use an Observer. If it's only needed in specific scenarios, use Events instead.
Example:
- User always needs a profile â Observer â
- User might get a welcome email depending on signup source â Event ðŊ
Tip 3: Observers Can Access Dirty Data
This is POWERFUL:
public function updated(User $user)
{
// Check what changed
if ($user->isDirty('email')) {
// Email changed!
}
// Get old value
$oldEmail = $user->getOriginal('email');
$newEmail = $user->email;
// Get all changes
$changes = $user->getDirty();
}
In production systems I've built, this saved us countless hours debugging "what changed?" issues.
Tip 4: Observers Fire on Mass Operations (Usually)
Here's a gotcha:
// This DOES fire observers (one by one)
User::where('active', false)->get()->each->delete();
// This does NOT fire observers (direct SQL)
User::where('active', false)->delete();
Choose wisely based on your needs! Mass deletes are faster but skip Observers.
Real-World Example: E-commerce Order Observer ð
Here's something similar to what I built in a serverless e-commerce backend:
class OrderObserver
{
public function creating(Order $order)
{
// Auto-generate order number before saving
$order->order_number = 'ORD-' . strtoupper(Str::random(10));
// Set initial status
$order->status = 'pending';
}
public function created(Order $order)
{
// Queue inventory reservation
ReserveInventory::dispatch($order);
// Send confirmation email
Mail::to($order->customer)->queue(new OrderConfirmation($order));
// Notify warehouse
event(new NewOrderPlaced($order));
}
public function updating(Order $order)
{
// Track status changes
if ($order->isDirty('status')) {
$order->status_changed_at = now();
$order->status_changed_by = auth()->id();
}
}
public function updated(Order $order)
{
// Status changed? Take action!
if ($order->wasChanged('status')) {
match($order->status) {
'paid' => event(new OrderPaid($order)),
'shipped' => Mail::to($order->customer)
->queue(new OrderShipped($order)),
'delivered' => AskForReview::dispatch($order)
->delay(now()->addDays(3)),
default => null,
};
}
}
}
This Observer handles ALL order lifecycle management. Controllers stay thin, business logic stays organized. Chef's kiss! ðĻâðģð
Common Mistakes (That I've Made) ð
Mistake 1: Heavy Operations in Observers Don't run slow stuff directly in Observers - queue it!
// Bad: Blocks the request
public function created(User $user)
{
$this->someSlowApiCall($user);
}
// Good: Queued for background processing
public function created(User $user)
{
ProcessNewUser::dispatch($user);
}
Mistake 2: Infinite Loops
public function updated(User $user)
{
// DON'T DO THIS! Infinite loop!
$user->update(['last_modified' => now()]);
}
Use updateQuietly() or check isDirty() to avoid loops:
public function updated(User $user)
{
if (!$user->isDirty('last_modified')) {
$user->updateQuietly(['last_modified' => now()]);
}
}
Mistake 3: Testing Nightmares Remember to account for Observers in tests! Sometimes you want them, sometimes you don't:
// Disable observers for a test
User::withoutEvents(function () {
User::factory()->count(100)->create();
});
The Bottom Line ð
Observers are like having a responsible roommate who automatically does the dishes, takes out the trash, and pays the bills on time. You barely notice them, but life is so much better with them!
Use Observers when:
- Something ALWAYS happens with a model
- You want to keep controllers thin
- You need automatic cleanup
- You're tracking changes for audit trails
Skip Observers when:
- Logic is context-specific (use Events)
- You need external input (use explicit methods)
- It's a one-off operation (just do it inline)
Quick Setup Checklist â
- Create Observer:
php artisan make:observer XObserver --model=X - Add logic to appropriate methods (
created,updated, etc.) - Register in
AppServiceProvider:X::observe(XObserver::class); - Test it! (Don't forget this one! ð )
- Queue heavy operations - don't block requests!
Questions about Observers? Hit me up on LinkedIn. As a Technical Lead who's built production systems handling real traffic, I've probably debugged the same Observer issues you're facing! ð
Want more Laravel magic? Star this blog on GitHub - more clean code patterns coming soon!
Now go make your controllers skinny and your Observers mighty! ðŠâĻ