ThinkPHP6.x模型事件需手动注册且严格匹配生命周期钩子。必须在init()中调用self::event()或快捷方法注册,子类需显式调用parent::init();beforeWrite嵌套beforeInsert/beforeUpdate,afterWrite与afterUpdate触发时机不同,绕过模型的操作不触发事件。

ThinkPHP6.x 的模型事件不是“写了就跑”的自动机制,而是需要明确注册、严格匹配、精准调用的生命周期钩子。用对了,业务逻辑干净解耦;用错了,连输出都看不到。
必须在 init() 里注册,否则事件不生效
模型事件不会自动扫描方法名,也不会按命名约定绑定。你写一个 public static function onBeforeInsert(),如果不手动注册,它就是普通静态方法,完全不会触发。
- 正确做法:在模型类中定义 protected static function init(),并在其中调用 self::event('before_insert', [...]) 或快捷方式 self::beforeInsert(...)
- 子类继承父类模型时,init() 不会自动调用 parent::init(),必须显式补上 parent::init();
- 避免在控制器、构造函数或 service 中临时注册——这些位置不可靠,且可能重复绑定或漏注册
区分 beforeWrite / beforeInsert / beforeUpdate 的触发时机
它们不是并列关系,而是嵌套触发:
- beforeWrite 是最外层钩子,所有写入操作(insert/update)都先过它,适合统一字段预处理(如补日志字段、校验权限)
- 框架根据 $model->id 是否存在 或是否调用 isUpdate(true) 判断动作类型,再触发 beforeInsert 或 beforeUpdate
- 若需差异化处理,优先用 beforeInsert 和 beforeUpdate;若只是通用拦截(比如强制设置 operator_id),用 beforeWrite 更简洁
afterUpdate 和 afterWrite 不是同一回事
别混用,否则容易踩坑:
立即学习“PHP免费学习笔记(深入)”;
- afterUpdate 只在模型已存在主键、走更新流程时触发,$model->id 必然有值,适合做关联缓存刷新、通知下游系统
- afterWrite 在新增和更新后都会触发,但内部需手动判断:$model->isUpdate() ? 'update' : 'insert',适合统一记录操作日志、清通用缓存
- 注意:afterWrite 执行时事务尚未提交,外部服务查不到新数据;若发 HTTP 请求或写 MQ,要考虑事务回滚导致的数据不一致
避开高频失效场景
90% 的“事件没反应”问题,都出在这几个地方:
- 用了 Db::name('user')->where(...)->update(...) —— 这绕过模型实例,事件完全不触发
- 批量操作如 UserModel::where()->update() 或 saveAll() —— 不创建单个模型对象,不走生命周期,只执行原生 SQL
- 在 after_delete 里尝试 $this->find() —— 此时数据已删,查不到是正常设计,不是 bug
- 在 retrieved 或 after_read 中修改属性,但没调 $model->setDirty('field', $value) —— 后续 save() 不会把该字段写进 SQL



















