Model events and observers
Before a post is saved, an address must be built from its title; after deletion, the image erased; when the price changes, the old one recorded in history. Instead of remembering this in every controller, the action is tied to a model event.
The events
A model reports every step of its life:
| Event | When it happens |
|---|---|
creating, created | Before and after a new record is added |
updating, updated | Before and after an existing one is changed |
saving, saved | Before and after any save |
deleting, deleted | Before and after deletion |
restoring, restored | Before and after a soft-deleted record is restored |
Events ending in -ing happen before the database query: data can still be changed there, or the action cancelled.
A handler in the model
Simple handlers are written in the booted() method:
use Illuminate\Support\Str;
class Post extends Model
{
protected static function booted(): void
{
static::creating(function (Post $post) {
$post->slug = Str::slug($post->title);
});
static::deleting(function (Post $post) {
$post->comments()->delete();
});
}
}Now a post always gets its address — wherever it is created from.
Cancelling an action
If the handler of an -ing event returns false, the action does not happen:
static::deleting(function (Post $post) {
if ($post->is_pinned) {
return false;
}
});An observer
When there are many handlers, they move into a separate class:
php artisan make:observer PostObserver --model=Postnamespace App\Observers;
use App\Models\Post;
use Illuminate\Support\Facades\Storage;
class PostObserver
{
public function updating(Post $post): void
{
if ($post->isDirty('price')) {
$post->priceHistory()->create(['price' => $post->getOriginal('price')]);
}
}
public function deleted(Post $post): void
{
Storage::disk('public')->delete($post->cover);
}
}isDirty() tells whether a field changed, getOriginal() returns the previous value.
Attaching the observer
use App\Observers\PostObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([PostObserver::class])]
class Post extends Model
{
// …
}Saving without events
Sometimes handlers get in the way — when loading test data, for instance:
$post->saveQuietly();
Post::withoutEvents(function () {
Post::factory()->count(100)->create();
});Common mistake
Events fire only when the action goes through a model. Mass queries bypass them:
Post::where('user_id', 5)->delete(); // no events
Post::where('user_id', 5)->get()->each->delete(); // events fireThe first query removes the rows with one DELETE without calling deleting — the images stay on disk. If data integrity depends on an event, delete and change records one at a time, or do the needed work next to the mass query.