Model Events & Observers

Hook into the lifecycle — creating, updated, deleting — without cluttering your controllers.

Every Eloquent model fires events as it moves through its life: creating, created, updating, updated, saving, saved, deleting, deleted, restored, and more. Listening to these is how you keep side effects (slugs, audit logs, cache busting) out of your business code.

Where to put the logic

  • Closures on the model's booted() — fine for one small thing.
  • An Observer class — the clean home once you have several hooks. One class, one responsibility.
// Small and local
protected static function booted()
{
    static::creating(function (Post $post) {
        $post->slug ??= \Illuminate\Support\Str::slug($post->title);
    });
}

The gotchas that bite people

The *ing events fire before the write and can cancel it by returning false. The *ed events fire after. And crucially: bulk operations like Post::where(...)->update([...]) or truncate() do not fire model events at all, because they never hydrate a model.

Example

Example · php
<?php

namespace App\Observers;

use App\Models\Order;
use Illuminate\Support\Facades\DB;

class OrderObserver
{
    public function creating(Order $order): void
    {
        // Runs before INSERT; can mutate the model or return false to cancel.
        $order->reference ??= 'ORD-' . strtoupper(\Illuminate\Support\Str::random(10));
    }

    public function updated(Order $order): void
    {
        // Only react to a real status change, not every save.
        if ($order->wasChanged('status') && $order->status === 'paid') {
            // Defer side effects until the surrounding transaction commits.
            DB::afterCommit(function () use ($order) {
                \App\Jobs\SendReceipt::dispatch($order);
            });
        }
    }

    public function deleting(Order $order): void
    {
        if ($order->status === 'shipped') {
            throw new \DomainException('Cannot delete a shipped order.');
        }
    }
}

// --- Registering via attribute (Laravel 11+/12) ---
namespace App\Models;

use App\Observers\OrderObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy([OrderObserver::class])]
class Order extends Model
{
    // Remember: Order::where(...)->update([...]) will NOT fire these hooks.
}

When to use it

  • A PostObserver's creating() hook automatically generates a URL slug from the title before any new Post row is inserted, without cluttering the controller.
  • An OrderObserver's updated() method fires a webhook notification when an order's status changes to 'shipped', keeping the controller free of notification logic.
  • A UserObserver's deleted() hook removes the user's S3 avatar file when the account is deleted, ensuring storage is not orphaned.

More examples

Generate and register an observer

Artisan generates an observer class pre-bound to the Post model with empty hooks for creating, created, updating, updated, deleting, and deleted.

Example · bash
php artisan make:observer PostObserver --model=Post
# Creates app/Observers/PostObserver.php

Auto-generate a slug in creating()

The creating() hook fires before INSERT, letting the observer set the slug without any controller involvement; deleting() cascades cleanup.

Example · php
class PostObserver
{
    public function creating(Post $post): void
    {
        if (empty($post->slug)) {
            $post->slug = Str::slug($post->title);
        }
    }

    public function deleting(Post $post): void
    {
        // Clean up related media files
        $post->media()->delete();
    }
}

Register the observer in a service provider

Observers are wired up in AppServiceProvider::boot(), or with the #[ObservedBy] attribute on the model in Laravel 10+.

Example · php
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Post::observe(PostObserver::class);
    Order::observe(OrderObserver::class);
}

// Or register via the model itself (Laravel 10+)
#[ObservedBy(PostObserver::class)]
class Post extends Model {}

Discussion

  • Be the first to comment on this lesson.