$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[May 23, 2026] Eloquent ORM

Eloquent Observers in Practice

Eloquent Observers: Tips & Tricks

────────────────────────────────────────────────────────

Eloquent Observers: Tips & Tricks

Tip: One Observer Per Model

Keep observers focused. One observer class handles all events for one model.

php artisan make:observer PostObserver --model=Post

Gotcha: Observers Run on Every Instance

Every time a model is created, updated, or deleted, the observer fires. For bulk operations, use withoutEvents().

Tip: Use wasChanged() in Updated Events

public function updated(Post $post): void
{
    if ($post->wasChanged('status')) {
        // Status changed
    }
}

Gotcha: Observer Registration

In Laravel 11+, observers are auto-discovered if named {Model}Observer. Otherwise, register manually:

Post::observe(PostObserver::class);

Tip: creating vs created

public function creating(Post $post): void
{
    // Before save — modify attributes
    $post->slug = Str::slug($post->title);
}

public function created(Post $post): void
{
    // After save — side effects
    Notification::send($post->author, new PostPublished($post));
}

Gotcha: Observers and Transactions

If your observer sends an email and the transaction rolls back, the email was already sent.

Tip: Silent Operations

$post->updateWithoutTouching(['title' => 'New Title']);
// Doesn't fire updated event, doesn't touch updated_at

Tip: Multiple Models, One Observer

class ContentObserver
{
    public function creating(Model $model): void
    {
        $model->slug = Str::slug($model->title ?? $model->name);
    }
}

Post::observe(ContentObserver::class);
Page::observe(ContentObserver::class);

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Observers are a clean way to organize model event logic, but they create a problem: model events become invisible in the controller flow. I've seen developers add a created observer without realizing it, then spend hours debugging why extra emails were sent on user registration. My practice: keep observer logic minimal (logging, cache invalidation) and put side-effect-heavy operations in dedicated services or queued listeners where the flow is explicit.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts