DEV Community

Cover image for The Complete Guide to Laravel Eloquent Boot Methods
Shakil Alam
Shakil Alam

Posted on • Edited on

The Complete Guide to Laravel Eloquent Boot Methods

If you've ever stared at an Eloquent model thinking "Wait, when exactly is my event running?" — this guide is for you.

Every Eloquent model in Laravel has a lifecycle: creation, update, deletion, query-building. Booting is a static process Laravel runs once per model class — cached for the entire request cycle — to register events, apply global scopes, and set up shared behavior. It does not run on every model instantiation. Understanding the boot order is what separates fragile code from rock-solid models.


The Three Boot Methods at a Glance

Method When it runs Best used for
Trait boot* First (static, once per class) Shared event/scope logic across models
boot() Second (static, once per class) Global scopes, complex boot logic
booted() Last (static, once per class) Simple event registration — the safer default
Trait initialize* Every instantiation Per-instance default property values

boot() — The Original, Lower-Level Hook

boot() is a static method on the model itself. It runs before booted() and is the right place for anything that needs to execute early in the chain.

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function boot()
    {
        parent::boot(); // 🚨 Always call this

        static::creating(function ($product) {
            $product->uuid = (string) \Str::uuid();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Key rules:

  • Always call parent::boot() — skipping it breaks trait boots and framework features like Soft Deletes
  • Use it for global scopes, which must be applied early in the chain
  • Runs once per model class in a static context

booted() — The Cleaner, Safer Default

Introduced in Laravel 7, booted() runs after the model has been fully booted. For most event registration, this is the method you should reach for first.

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function booted()
    {
        static::creating(function ($product) {
            $product->uuid = (string) \Str::uuid();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Why prefer booted() for events:

  • No need to call parent::booted() — it chains automatically
  • Safer in team environments where multiple developers add boot logic
  • Cannot accidentally overwrite another developer's boot() logic

Trait Boot Methods — Shared Logic Across Models

If a trait defines a method named boot[TraitName], Laravel calls it automatically — before the model's own boot(). This is how features like SoftDeletes work under the hood.

trait HasUuid
{
    protected static function bootHasUuid()
    {
        static::creating(fn($model) => $model->uuid = (string) \Str::uuid());
    }
}

class Product extends Model
{
    use HasUuid;
}
Enter fullscreen mode Exit fullscreen mode

The method name must follow the pattern: boot + the trait name in camel case.


initialize[TraitName]() — The Instance-Level Counterpart

This is commonly overlooked and frequently confused with bootTraitName. The difference is important:

  • boot[TraitName]() — static, runs once per class per request cycle, used for events and scopes
  • initialize[TraitName]() — instance-level, runs on every new model instantiation, used for default property values
trait HasDefaultStatus
{
    public function initializeHasDefaultStatus()
    {
        // Runs every time `new Product()` is called
        $this->attributes['status'] = $this->attributes['status'] ?? 'draft';
    }
}
Enter fullscreen mode Exit fullscreen mode

Use initialize[TraitName]() when you need to set per-instance defaults rather than register static behavior.


The Boot Sequence (Absolute Order)

[Trait boot* methods] → [Model::boot()] → [Model::booted()]
Enter fullscreen mode Exit fullscreen mode

This order never changes. Everything you build should respect it.


Real-World Example: All Three Together

trait HasUuid
{
    protected static function bootHasUuid() // Runs first
    {
        static::creating(fn($model) => $model->uuid = (string) \Str::uuid());
    }
}

class Product extends Model
{
    use HasUuid;

    protected static function boot() // Runs second
    {
        parent::boot(); // Required

        static::creating(fn($product) => $product->slug = \Str::slug($product->name));
    }

    protected static function booted() // Runs last
    {
        static::created(fn($product) => logger("Product {$product->id} created!"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Execution order: bootHasUuid sets the UUID → boot() sets the slug → booted() logs the creation. All events fire in the right order, nothing breaks.


Common Pitfalls

Forgetting parent::boot() in boot()
Trait boots won't run. This silently breaks features like Soft Deletes.

Applying global scopes in booted() — mostly fine, but know the nuance
In Laravel 8+, global scopes are applied lazily at query time, so registering them in booted() works in most cases. However, if other trait boot* methods depend on a scope being present, boot() is the safer choice since it runs earlier in the chain.

Multiple developers overriding boot()
Each override can overwrite the previous one. Move event registration to booted() to avoid this entirely.


The One-Line Takeaway

Use booted() for events. Use boot() for global scopes. Use trait boots for logic that needs to be shared across multiple models. Always call parent::boot().

Mastering this sequence is the difference between code that seems to work and models you can trust in production.

Top comments (4)

Collapse
 
xwero profile image
david duymelinck

Scopes need boot()

So you are saying the official documentation got it wrong?
It clearly states use the ScopedBy attribute or add them to the booted method.

Multiple developers overriding boot() → Can overwrite each other's logic; booted() avoids this problem.

How is this a problem? There is only one boot method per model.
If that would be a problem it would also be a problem for the booted method.

Collapse
 
itxshakil profile image
Shakil Alam

Thanks for the thoughtful comment, David — that’s a really good point!

You’re right that each model only defines one boot() method, but the issue isn’t about having multiple boot() methods at the same time — it’s about how easy it is to accidentally break the boot chain when multiple developers touch the same model.

The boot() method participates in Laravel’s inheritance chain, which means every time you override it, you must call parent::boot() to ensure all trait boot methods (bootSoftDeletes, bootHasUuid, etc.) and parent model logic still execute. If even one developer forgets that call or replaces the method entirely, it overwrites the existing behavior — and that’s where problems creep in.

In contrast, booted() doesn’t rely on inheritance chaining at all. Laravel automatically collects and executes all booted() callbacks from the model and its traits. This makes it additive rather than fragile, which is why it’s safer when multiple developers or traits contribute boot logic to the same model.

So when I say:

“Multiple developers overriding boot() → Can overwrite each other’s logic; booted() avoids this problem.”

I don’t mean multiple methods literally exist — I mean boot() is prone to human error and overwritten logic due to inheritance, while booted() is designed to prevent that by construction.

👉 For global scopes, they should always be defined inside boot(), because scopes must attach before the model is fully booted to apply consistently across all queries. For example:

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('active', function ($builder) {
        $builder->where('is_active', true);
    });
}

Meanwhile, booted() is ideal for lightweight event registration that happens after booting:

protected static function booted()
{
    static::created(fn ($model) => logger("{$model->getTable()} created: {$model->id}"));
}

In short:

  • boot() → for global scopes and early initialization (must call parent::boot()).
  • booted() → for post-boot events and cleaner, additive logic.

Both have their place — boot() gives you control, but booted() gives you safety.

Collapse
 
xwero profile image
david duymelinck

which means every time you override it

How can you override it many times? Can you give an example?

For global scopes, they should always be defined inside boot()

The example in the Laravel documentation is

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see it on their website. Why would they spread bad information about their own framework?

Thread Thread
 
itxshakil profile image
Shakil Alam

it’s not referring to multiple boot methods inside the same model class (because, as you said, there can only be one). The situation arises when traits or parent classes define their own boot() methods. PHP allows only one boot() method per final class, so if multiple traits define boot() directly, the last one wins, and earlier logic may be silently overwritten.

Here’s a minimal example:

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

trait LogsCreation {
    protected static function boot(): void {
        static::creating(fn ($model) => logger("Creating model {$model->id}"));
    }
}

class User extends Model
{
    use HasUuid, LogsCreation;

    protected static function boot(): void {
        parent::boot();
        static::creating(fn ($model) => $model->name = strtoupper($model->name));
    }
}
Enter fullscreen mode Exit fullscreen mode

Depending on trait resolution, some boot logic (like UUID generation or logging) may not run because only one boot() method exists in the final class.

The safe alternative is:

  1. Use bootTraitName() in traits (Laravel automatically calls them), or
  2. Use booted() in the model for event callbacks, which allows multiple hooks to coexist safely:
protected static function booted(): void {
    static::creating(fn ($model) => $model->name = strtoupper($model->name));
}
Enter fullscreen mode Exit fullscreen mode

Note: For global scopes, boot() is still recommended because they must exist before booted() callbacks.

I’m not denying the usefulness of boot() — it’s essential for global scopes, parent boot logic, or other critical pre-boot operations. My point is only that for multiple event callbacks (especially with traits), booted() is safer because it avoids subtle overwriting issues.