Eager loading — with(), the N+1 problem explained
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. Returnfalsefromcreatingto 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:
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
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