Under the Hood

Model Events - When onCreate, onUpdate Fire

Learn when Laravel model events fire and how to use them. Understand creating, created, updating, updated, saving, saved, deleting, and deleted events in Laravel 12.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
12-Dec-2025
7 min read
Model Events - When onCreate, onUpdate Fire

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:

  1. retrieved - After model fetched from database
  2. creating - Before INSERT query
  3. created - After INSERT query
  4. updating - Before UPDATE query
  5. updated - After UPDATE query
  6. saving - Before INSERT or UPDATE
  7. saved - After INSERT or UPDATE
  8. deleting - Before DELETE query
  9. deleted - After DELETE query
  10. 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:

  1. saving - Fires first, before any database operation
  2. creating - Fires next, specifically before INSERT
  3. Database INSERT executes
  4. created - Fires after INSERT succeeds
  5. 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:

  1. saving - Fires first
  2. updating - Fires before UPDATE query
  3. Database UPDATE executes
  4. updated - Fires after UPDATE succeeds
  5. 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:

  1. deleting - Fires before DELETE
  2. Database DELETE executes
  3. 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.


Article Tags

laravel model events eloquent events model event lifecycle laravel event hooks laravel model observers

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products