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();
});
}
}
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();
});
}
}
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;
}
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';
}
}
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()]
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!"));
}
}
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)
So you are saying the official documentation got it wrong?
It clearly states use the
ScopedByattribute or add them to the booted method.How is this a problem? There is only one
bootmethod per model.If that would be a problem it would also be a problem for the
bootedmethod.How can you override it many times? Can you give an example?
The example in the Laravel documentation is
You can see it on their website. Why would they spread bad information about their own framework?
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 oneboot()method per final class, so if multiple traits defineboot()directly, the last one wins, and earlier logic may be silently overwritten.Here’s a minimal example:
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:
bootTraitName()in traits (Laravel automatically calls them), orbooted()in the model for event callbacks, which allows multiple hooks to coexist safely:Note: For global scopes,
boot()is still recommended because they must exist beforebooted()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.