0

Eager loading — with(), the N+1 problem explained

Intermediate5 min read·lv-12-010
sqlinterviewperformance

Concept

Model events fire at key lifecycle points. Observers group multiple event listeners for a model into a single class, keeping model code clean.

Built-in model events:

  • creating / created: Before/after INSERT. Return false from creating to cancel.
  • updating / updated: Before/after UPDATE.
  • saving / saved: Before/after INSERT OR UPDATE (fires for both).
  • deleting / deleted: Before/after DELETE.
  • restoring / restored: Before/after soft-delete restore.
  • retrieved: After a model is fetched from the database (fires on each row in a collection).
  • replicating: Before $model->replicate().

Defining listeners in the model:

php
protected static function boot(): void {
    parent::boot();
    static::creating(fn($model) => $model->uuid = Str::uuid());
}

Observers: A class with a method per event (creating, created, etc.). Registered in AppServiceProvider::boot() via User::observe(UserObserver::class) or in the model with #[ObservedBy] attribute.

The ObservedBy attribute (Laravel 10+): #[ObservedBy([UserObserver::class])] on the model class — no service provider registration needed.

withoutEvents(): Execute code without firing model events: User::withoutEvents(fn() => User::create([...])). Useful in seeders and migrations.

isDirty() / isClean() / getOriginal() / getChanges(): Available during updating/updated events to inspect what changed.

Code Example

php
<?php
namespace App\Observers;

use App\Models\User;

class UserObserver
{
    public function creating(User $user): void
    {
        // Generate UUID before inserting
        $user->uuid = (string) \Illuminate\Support\Str::uuid();
    }

    public function created(User $user): void
    {
        // Send welcome email
        \Illuminate\Support\Facades\Mail::to($user)->queue(new \App\Mail\WelcomeEmail($user));
    }

    public function updating(User $user): void
    {
        // Track email changes
        if ($user->isDirty('email')) {
            $user->email_verified_at = null; // require re-verification
        }
    }

    public function updated(User $user): void
    {
        // Log changes
        \Illuminate\Support\Facades\Log::info('User updated', [
            'id'      => $user->id,
            'changes' => $user->getChanges(),  // only changed attributes
            'original' => $user->getOriginal(), // original values
        ]);
    }

    public function deleting(User $user): bool|null
    {
        // Prevent deletion if user has active orders
        if ($user->orders()->where('status', 'active')->exists()) {
            return false; // cancel deletion
        }
        return null; // allow
    }

    public function deleted(User $user): void
    {
        // Cleanup related data
        $user->profile()->delete();
    }
}

// Register in AppServiceProvider::boot()
User::observe(UserObserver::class);

// Or use attribute (Laravel 10+)
#[\Illuminate\Database\Eloquent\Attributes\ObservedBy([UserObserver::class])]
class User extends \Illuminate\Database\Eloquent\Model {}

// Bypass events in seeders
User::withoutEvents(function() {
    User::factory()->count(100)->create(); // no welcome emails fired
});

// Check what's changing during an update
$user = User::find(1);
$user->name = 'New Name';
$user->isDirty('name');    // true — changed but not yet saved
$user->isDirty('email');   // false — not changed
$user->save();
$user->wasChanged('name'); // true — was saved with a change