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
save() / delete()
created / updated / deleted
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.
Pro tip: Use getChanges() on the updated event, not getDirty(). The difference is that getChanges() only returns fields that were actually saved to the database, while getDirty() returns fields that were changed in memory but may not have been persisted yet.
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) |
Rule of thumb: Use a Laravel Observer when your app is the only thing writing to the database, which covers most Laravel projects. Use a MySQL Trigger when multiple systems write to the same table and you need logging regardless of source.
Watch out: Observer methods do not fire when you use mass-update or mass-delete queries like User::where(...)->update([...]) or User::where(...)->delete(). These bypass Eloquent entirely. Always use model instances if you need observers to fire.
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.