Eloquent 模型在每次实例化时都会执行两个阶段:「启动处理(boot)」和「初始化处理(initialize)」。只要在 trait 中定义符合规约的方法,模型 use 该 trait 时就会自动调用它们。
利用这一机制,可以干净地实现向模型注入功能的 trait。Laravel 框架自身也在 SoftDeletes、HasUuids、HasUlids 等大量 trait 中广泛运用此模式。
boot 与 initialize 的区别
| bootXxx() | initializeXxx() |
|---|
| 调用时机 | 模型类首次启动时(每个类一次) | 每次创建实例时 |
| 方法类型 | static | 实例方法 |
| 典型用途 | 注册事件监听器、添加全局作用域 | 设置默认属性值、合并 casts |
命名规约
bootXxx()
在 trait 名称前加上 boot 前缀,定义 static 方法,即可在模型启动时被调用一次。
namespace App\Models\Concerns;
trait HasSlug
{
public static function bootHasSlug(): void
{
static::creating(function ($model) {
if (empty($model->slug)) {
$model->slug = str($model->name)->slug()->toString();
}
});
}
}
在模型中使用:
use App\Models\Concerns\HasSlug;
class Article extends Model
{
use HasSlug;
// bootHasSlug() 会被自动调用
}
initializeXxx()
在 trait 名称前加上 initialize 前缀,定义实例方法,即可在每次 new 模型时被调用。
namespace App\Models\Concerns;
trait HasDefaultStatus
{
public function initializeHasDefaultStatus(): void
{
$this->attributes['status'] ??= 'draft';
}
}
也可以用于动态添加 $casts:
trait HasMetadata
{
public function initializeHasMetadata(): void
{
$this->mergeCasts([
'metadata' => 'array',
]);
}
}
通过 PHP Attribute 显式指定
从 Laravel 11 起,可以不依赖命名规约,通过 PHP Attribute 显式标注方法。
| Attribute | 用途 |
|---|
#[Boot] | 标记在类启动时仅执行一次的方法 |
#[Initialize] | 标记在每次创建实例时执行的方法 |
use Illuminate\Database\Eloquent\Attributes\Boot;
use Illuminate\Database\Eloquent\Attributes\Initialize;
trait HasAuditLog
{
#[Boot]
public static function registerAuditListeners(): void
{
static::created(function ($model) {
AuditLog::record('created', $model);
});
static::updated(function ($model) {
AuditLog::record('updated', $model);
});
}
#[Initialize]
public function setAuditDefaults(): void
{
$this->attributes['audit_enabled'] ??= true;
}
}
使用 PHP Attribute 后,方法名不再受 trait 名称的约束,可以取更有语义的名字。也可以在同一个 trait 中定义多个 #[Boot] 或 #[Initialize] 方法。
实用示例:包开发中的应用
自动添加全局作用域
trait HasTenant
{
public static function bootHasTenant(): void
{
static::addGlobalScope(new TenantScope);
}
}
添加默认 cast
trait HasJsonSettings
{
public function initializeHasJsonSettings(): void
{
$this->mergeCasts([
'settings' => 'array',
]);
}
}
通过 Eloquent 事件自动处理
trait Archivable
{
#[Boot]
public static function bootArchivable(): void
{
static::deleting(function ($model) {
Archive::store($model->toArray());
});
}
}
Laravel 框架内的示例
Laravel 框架本身在众多 trait 中都采用了这一模式。
| Trait | 方法 | 处理内容 |
|---|
SoftDeletes | bootSoftDeletes() | 添加软删除用的全局作用域 |
HasFactory | bootHasFactory() | 无实质处理(作用域外机制) |
HasUlids | bootHasUlids() | 在 creating 事件中自动设置 ULID |
HasUuids | bootHasUuids() | 在 creating 事件中自动设置 UUID |
阅读这些参考实现,有助于加深对该模式的理解。
注意事项
清除模型缓存
由于 boot* 每个类只执行一次,在测试环境中需要特别注意。可以通过 Model::clearBootedModels() 重置已启动的缓存。
// 测试结束后重置模型的启动状态
protected function tearDown(): void
{
Model::clearBootedModels();
parent::tearDown();
}
执行顺序
当同一模型 use 多个 trait 时,bootXxx() / initializeXxx() 会按 PHP 的 trait 解析顺序(即声明顺序)被调用。若 trait 之间存在依赖关系,请注意 use 的顺序。
| 模式 | 定义方式 | 适用场景 |
|---|
| 类启动时处理 | static bootXxx() 或 #[Boot] | 事件注册、添加全局作用域 |
| 实例初始化 | initializeXxx() 或 #[Initialize] | 默认值、添加 cast |
在 trait 中实现 bootXxx() / initializeXxx(),就能通过一行 use 向模型注入功能。这是包开发中向模型追加功能的经典模式。