应使用模型类的booted方法注册static::created、static::updated等静态事件钩子;软删除需监听trashed而非deleted;事件中修改字段须加isDirty守卫防死循环;跨进程通知需Redis Pub/Sub或消息队列。

监听模型保存前后的事件用什么方法
直接用 booted 里注册 creating、updated 这类静态事件钩子,别在构造函数或普通方法里绑——模型实例还没初始化完,事件根本不会触发。
常见错误是写成 $this->created(...) 或试图在控制器里调用 Product::created(...),这俩都无效:前者没绑定到静态事件系统,后者语法错(created 是静态方法,但不能这样链式调用)。
- 必须在模型类的
booted方法里注册,比如:protected static function booted() { static::updated(fn ($model) => event(new ProductUpdated($model))); } - 如果要区分“首次创建”和“后续更新”,用
created+updated,别只靠saving判断$model->exists—— 它在批量插入时可能不准 - 事件名建议用自定义事件类(如
ProductUpdated),别直接塞业务逻辑进闭包,否则测试难、复用差、队列序列化可能失败
变更通知发太频繁怎么节流
用户改个标题点十次保存,你发十次 Slack 通知,这不是通知是骚扰。Laravel 自带事件不带节流,得自己加判断逻辑。
典型场景是「字段值实际没变」还触发了 updated:比如时间戳自动更新、JSON 字段顺序重排导致 toJson() 结果不同,但业务上数据没变。
- 在事件回调里对比关键字段,用
$model->getOriginal('price')和$model->price判断是否真变了 - 避免用
json_encode($model->getChanges())做全量比对——浮点数精度、空数组/对象差异、时间格式都容易误判 - 如果通知依赖外部 API(比如钉钉机器人),把发送逻辑推到队列,并在 job 里加 Redis 键做 5 秒去重:
Cache::put('notify_'.$model->id, true, 5),存在就跳过
软删除模型的 deleted 事件为什么没触发
因为 deleted 默认只响应硬删除(forceDelete())。调用 delete() 实际触发的是 deleting → deleted 吗?不是,它走的是 trashed 事件链。
如果你监听了 deleted 却收不到通知,大概率模型启用了 SoftDeletes trait,而你没意识到 Laravel 把软删拆成了两套事件。
- 软删生效时触发的是
trashed(删除前)和restored(恢复时),不是deleted - 要同时覆盖硬删和软删,得注册三个事件:
deleted、trashed、forceDeleted -
forceDeleted是 8.0+ 才有的,低版本只能靠重写forceDelete()方法手动 dispatch 事件
事件里修改模型字段会引发二次保存吗
会,而且很可能死循环。比如在 saving 回调里改 $model->slug = Str::slug($model->title),接着模型继续走保存流程,又进一次 saving —— 如果没加 guard,就套娃了。
这不是 Laravel 的 bug,是事件设计使然:所有生命周期钩子都在 ORM 流程中同步执行,没有“只读上下文”。
- 改字段必须加守卫,比如
if (!$model->isDirty('slug')) { $model->slug = ... } - 更安全的做法是把字段生成逻辑移到
creating/updating,这两个事件只在真正要存库前触发一次 - 绝对不要在
saved或updated里调$model->save()—— 此时事务已提交,再 save 就是新事务,极难回滚,也容易漏掉验证规则
最常被忽略的一点:模型事件默认不跨进程生效。队列任务里修改模型,主进程监听不到;API 请求里 dispatch 的事件,WebSocket 进程也不会收到。需要通知多端,得用 Redis Pub/Sub 或消息队列桥接,别指望一个 event() 调用全搞定。











