Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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:

EventWhen it happens
creating, createdBefore and after a new record is added
updating, updatedBefore and after an existing one is changed
saving, savedBefore and after any save
deleting, deletedBefore and after deletion
restoring, restoredBefore 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=Post
namespace 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 fire

The 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.

Thanks for reading!

Donate to the site — we work hard for you.