The Problem It Solves
Imagine you have an AuditLog table. Every time a user record changes, you want to save what changed, who changed it, and when.
The naive approach is to write the log call directly inside every controller method:
// Bad approach: you repeat this in every controller that touches User
public function update(Request $request, User $user): JsonResponse
{
$user->update($request->all());
// You manually log here
AuditLog::create([...]);
}
public function destroy(User $user): JsonResponse
{
$user->delete();
// You manually log here again, hoping you remembered
AuditLog::create([...]);
}
This gets messy fast. What if one developer forgets to add it? What if a queued job deletes the user in the background? The log gets missed entirely. You are relying on everyone remembering to do the right thing in every place.
The Observer Pattern removes that risk. Instead of calling the log yourself, you tell Laravel: whenever anything happens to this model, anywhere in the codebase, run this automatically.
How It Works
The flow has three moving parts:
- The model — a
Userinstance callssave()ordelete(). - The event — Laravel fires
created,updated, ordeleted. - The observer — your
UserObserverreacts viacreated(),updated(), ordeleted().
Three things are involved: the Subject (the model being watched), the Observer (your listener class), and the Event (the trigger like created or updated). Laravel handles wiring them together. You just define what to do when each event fires.
Laravel Implementation
Step 1 — Generate the Observer
php artisan make:observer UserObserver --model=User
Step 2 — Write the logic
namespace App\Observers;
use App\Models\AuditLog;
use App\Models\User;
class UserObserver
{
public function created(User $user): void
{
$this->log('created', $user);
}
public function updated(User $user): void
{
// getChanges() returns only fields that were actually saved to the DB
$this->log('updated', $user, $user->getChanges());
}
public function deleted(User $user): void
{
$this->log('deleted', $user);
}
private function log(string $action, User $user, array $changes = []): void
{
AuditLog::create([
'model' => 'User',
'model_id' => $user->id,
'action' => $action,
'old_values' => json_encode($user->getOriginal()),
'new_values' => json_encode($changes),
'changed_by' => auth()->id(),
'changed_at' => now(),
]);
}
}
Step 3 — Register it once
public function boot(): void
{
User::observe(UserObserver::class);
}
That is the entire setup. Now forget about it. Every create, update, or delete on the User model, whether it comes from a controller, a queued job, an artisan command, or an API endpoint written six months from now, is logged automatically.
Is This Industry Standard?
Yes, fully. Audit logging via model observers is one of the most common patterns in production Laravel applications. Every major framework has an equivalent: Symfony uses Doctrine Event Listeners, Django has Signals, Rails has ActiveRecord Callbacks, and Spring uses Application Events. The concept is universal.
It maps directly to the Single Responsibility Principle: your controller is responsible for handling the request, and your observer is responsible for reacting to model changes. Neither one knows about the other, which is exactly how it should be.
Observer vs MySQL Triggers
A MySQL Trigger can also log changes at the database level. Here is when each one actually makes sense:
| Capability | Laravel Observer | MySQL Trigger |
|---|---|---|
| Know who made the change | Yes via auth()->id() |
No — no app context |
| Send emails or queue jobs | Yes | No |
| Unit testable | Yes | Hard |
| Visible in codebase | Yes — version controlled | No — hidden in DB |
| Works if Eloquent is bypassed | No — raw SQL skips it | Yes |
| Works across multiple apps | No | Yes |
| Performance overhead | Slight PHP overhead | Faster (in-DB) |
Summary
The Observer Pattern is not a trick or an over-engineered solution. It is the correct tool for the job when you need to react consistently to model changes across your entire application. You write the logic once, register it once, and it works everywhere forever.
For audit logging specifically: use a Laravel Observer. It knows who made the change, lives in your codebase, is easy to test, and follows the same pattern every serious Laravel project uses. MySQL Triggers are only the right call in multi-system architectures where you cannot rely on Eloquent being in the picture.
Work with me
Need help with your Laravel architecture?
I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.