Laravel models silently broadcast events throughout their lifecycle—when creating, updating, deleting, or even just retrieving from the database. But when exactly do these events fire? Understanding the precise moment each event triggers unlocks powerful capabilities for logging, notifications, caching, and business logic automation.
What Are Model Events?
Model events are hooks that fire at specific points in a model's lifecycle:
class User extends Model
{
protected static function booted()
{
static::creating(function ($user) {
// Fires BEFORE user is inserted
$user->uuid = Str::uuid();
});
static::created(function ($user) {
// Fires AFTER user is inserted
Log::info("New user created: {$user->id}");
});
}
}
Events let you hook into model operations without cluttering controllers.
Available Model Events
Laravel provides 10 model events:
- retrieved - After model fetched from database
- creating - Before INSERT query
- created - After INSERT query
- updating - Before UPDATE query
- updated - After UPDATE query
- saving - Before INSERT or UPDATE
- saved - After INSERT or UPDATE
- deleting - Before DELETE query
- deleted - After DELETE query
- forceDeleted - After force delete (with soft deletes)
The Event Lifecycle
Creating a New Model
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
Event order:
- saving - Fires first, before any database operation
- creating - Fires next, specifically before INSERT
- Database INSERT executes
- created - Fires after INSERT succeeds
- saved - Fires last, after any save operation
// Behind the scenes
static::saving(function ($user) {
echo "1. saving\n";
});
static::creating(function ($user) {
echo "2. creating\n";
});
// INSERT INTO users...
static::created(function ($user) {
echo "3. created\n";
});
static::saved(function ($user) {
echo "4. saved\n";
});
Updating an Existing Model
$user = User::find(1);
$user->name = 'Jane Doe';
$user->save();
Event order:
- saving - Fires first
- updating - Fires before UPDATE query
- Database UPDATE executes
- updated - Fires after UPDATE succeeds
- saved - Fires last
// Behind the scenes
static::saving(function ($user) {
echo "1. saving\n";
});
static::updating(function ($user) {
echo "2. updating\n";
});
// UPDATE users SET...
static::updated(function ($user) {
echo "3. updated\n";
});
static::saved(function ($user) {
echo "4. saved\n";
});
Deleting a Model
$user = User::find(1);
$user->delete();
Event order:
- deleting - Fires before DELETE
- Database DELETE executes
- deleted - Fires after DELETE succeeds
static::deleting(function ($user) {
echo "1. deleting\n";
});
// DELETE FROM users...
static::deleted(function ($user) {
echo "2. deleted\n";
});
The retrieved Event
Fires every time a model is loaded from the database:
static::retrieved(function ($user) {
// Fires for EVERY query result
Log::debug("User {$user->id} retrieved");
});
// Triggers retrieved event
$user = User::find(1);
// Triggers retrieved event for EACH user
$users = User::all(); // Fires 100 times if 100 users
Performance warning: Be careful with expensive operations in retrieved events.
Event Registration Methods
Method 1: In booted() Method
class User extends Model
{
protected static function booted()
{
static::creating(function ($user) {
$user->uuid = Str::uuid();
});
}
}
Recommended: Clean, organized, lives with model.
Method 2: In Service Provider
// App\Providers\EventServiceProvider
use App\Models\User;
public function boot()
{
User::creating(function ($user) {
$user->uuid = Str::uuid();
});
}
Use when: Event logic is complex or involves multiple models.
Method 3: Observer Classes
// App\Observers\UserObserver
class UserObserver
{
public function creating(User $user)
{
$user->uuid = Str::uuid();
}
public function created(User $user)
{
Log::info("User created: {$user->id}");
}
}
// In service provider
User::observe(UserObserver::class);
Best for: Complex event logic with many handlers.
Preventing Events from Firing
Sometimes you need to save without triggering events:
// Skip all events
$user = User::withoutEvents(function () {
return User::create(['name' => 'Test']);
});
// Or for updates
User::withoutEvents(function () use ($user) {
$user->update(['name' => 'Updated']);
});
Useful for:
- Data migrations
- Seeding
- Bulk operations
- Testing
Stopping Event Propagation
Return false from an event to cancel the operation:
static::creating(function ($user) {
if ($user->email === 'banned@example.com') {
return false; // Cancel creation
}
});
// This won't be created
User::create(['email' => 'banned@example.com']);
// Returns false, no database INSERT
The save operation aborts, and subsequent events don't fire.
The saving vs creating Distinction
Both fire before save, but serve different purposes:
saving Event
static::saving(function ($user) {
// Fires for BOTH create and update
$user->updated_by = auth()->id();
});
Use for logic that applies to all saves.
creating Event
static::creating(function ($user) {
// Fires ONLY for create
$user->uuid = Str::uuid();
});
Use for logic specific to new records.
Common Pattern
static::saving(function ($user) {
// Apply to both create and update
$user->slug = Str::slug($user->name);
});
static::creating(function ($user) {
// Only for new records
$user->verification_token = Str::random(32);
});
Practical Use Cases
Auto-Generate UUIDs
static::creating(function ($model) {
if (empty($model->uuid)) {
$model->uuid = Str::uuid();
}
});
Auto-Generate Slugs
static::saving(function ($post) {
if ($post->isDirty('title')) {
$post->slug = Str::slug($post->title);
}
});
Activity Logging
static::created(function ($user) {
ActivityLog::create([
'type' => 'user_created',
'user_id' => $user->id,
'ip_address' => request()->ip(),
]);
});
static::updated(function ($user) {
ActivityLog::create([
'type' => 'user_updated',
'user_id' => $user->id,
'changes' => $user->getChanges(),
]);
});
Send Notifications
static::created(function ($user) {
Mail::to($user)->send(new WelcomeEmail($user));
});
static::updated(function ($user) {
if ($user->wasChanged('email')) {
Mail::to($user)->send(new EmailChangedNotification());
}
});
Cache Invalidation
static::saved(function ($product) {
Cache::forget("product:{$product->id}");
Cache::forget('products:all');
});
static::deleted(function ($product) {
Cache::forget("product:{$product->id}");
Cache::forget('products:all');
});
Cascade Deletes
static::deleting(function ($user) {
// Delete related records before user is deleted
$user->posts()->delete();
$user->comments()->delete();
});
Update Timestamps on Related Models
// Post model
static::saved(function ($post) {
// Touch parent category's updated_at
$post->category->touch();
});
Events That Don't Fire
These operations bypass model events:
// Mass updates - NO events
User::where('active', false)->update(['status' => 'inactive']);
// Mass deletes - NO events
User::where('created_at', '<', now()->subYear())->delete();
// Increment/decrement - NO events
$post->increment('views');
// Raw queries - NO events
DB::table('users')->where('id', 1)->update(['name' => 'Test']);
To fire events, load models first:
// With events
User::where('active', false)->get()->each(function ($user) {
$user->update(['status' => 'inactive']);
});
Checking What Changed
Inside event handlers, check what changed:
static::updating(function ($user) {
// Check if specific attribute changed
if ($user->isDirty('email')) {
// Email is being changed
$oldEmail = $user->getOriginal('email');
$newEmail = $user->email;
// Send verification email
}
});
static::updated(function ($user) {
// Get all changes
$changes = $user->getChanges();
// ['email' => 'new@example.com', 'updated_at' => '...']
// Check what was changed
if ($user->wasChanged('password')) {
// Password was changed
}
});
Performance Considerations
Retrieved Events Can Be Expensive
// Fires 1000 times!
static::retrieved(function ($user) {
// Don't do expensive operations here
Log::debug("Retrieved user {$user->id}"); // Database write per user!
});
$users = User::limit(1000)->get();
Events Add Overhead
// With events (5 events fire)
User::create(['name' => 'John']); // ~15-25ms
// Without events
User::withoutEvents(function () {
User::create(['name' => 'John']); // ~10-15ms
});
Use withoutEvents() for bulk operations.
Testing Model Events
public function test_uuid_is_generated_on_user_creation()
{
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->assertNotNull($user->uuid);
}
public function test_welcome_email_sent_on_user_creation()
{
Mail::fake();
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->user->id === $user->id;
});
}
Debugging Events
// Log all events for a model
static::retrieved(fn($m) => Log::debug('retrieved', ['id' => $m->id]));
static::creating(fn($m) => Log::debug('creating'));
static::created(fn($m) => Log::debug('created', ['id' => $m->id]));
static::updating(fn($m) => Log::debug('updating', ['id' => $m->id]));
static::updated(fn($m) => Log::debug('updated', ['id' => $m->id]));
static::saving(fn($m) => Log::debug('saving', ['id' => $m->id ?? 'new']));
static::saved(fn($m) => Log::debug('saved', ['id' => $m->id]));
static::deleting(fn($m) => Log::debug('deleting', ['id' => $m->id]));
static::deleted(fn($m) => Log::debug('deleted', ['id' => $m->id]));
Best Practices
1. Keep Event Handlers Fast
// Bad - blocking operation
static::created(function ($user) {
Mail::to($user)->send(new WelcomeEmail()); // Blocks for seconds
});
// Good - queue it
static::created(function ($user) {
Mail::to($user)->queue(new WelcomeEmail());
});
2. Use Observers for Complex Logic
// Bad - cluttered model
static::booted() {
static::creating(/* 50 lines */);
static::created(/* 50 lines */);
static::updating(/* 50 lines */);
}
// Good - organized observer
User::observe(UserObserver::class);
3. Avoid Side Effects in retrieved
// Bad - fires too often
static::retrieved(function ($user) {
Cache::put("user:{$user->id}", $user, 3600);
});
// Good - use dedicated caching
4. Handle Failures Gracefully
static::created(function ($user) {
try {
Mail::to($user)->send(new WelcomeEmail());
} catch (\Exception $e) {
Log::error("Failed to send welcome email", [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
// Don't throw - allow creation to succeed
}
});
Conclusion
Laravel model events provide elegant hooks into the model lifecycle, allowing you to automate business logic without cluttering controllers. Understanding when each event fires—and in what order—helps you build sophisticated applications with clean, maintainable code.
Remember: creating fires before INSERT, created after. updating before UPDATE, updated after. And saving/saved fire for both operations. Master this sequence, and you'll write powerful, event-driven Laravel applications.
Building Laravel applications with complex business logic? At NeedLaravelSite, we specialize in Laravel development and migrations from version 7 to 12. From event-driven architectures to clean code patterns, we build maintainable, scalable applications.