ThinkPHP模型事件仅在调用save()、delete()、find()等模型方法时触发,必须在模型init()静态方法中注册,支持beforeWrite(写入前统一拦截)、beforeInsert/beforeUpdate(类型明确后触发)等事件,且不响应Db类原生操作。

ThinkPHP 模型事件不是“所有操作都触发”,而是严格绑定在 save()、delete()、find() 等模型方法调用时才激活。漏掉初始化或写错方法名,事件根本不会执行。
模型事件的注册方式必须用 init() 静态方法
不能在构造函数 __construct() 里注册,也不能在控制器里用 self::event() 临时挂载(除非是单次测试)。正确入口只有模型类的 init() 方法:
-
init()是静态方法,必须声明为protected static function init() - 注册用
self::event('before_update', [...])或快捷静态钩子如onBeforeUpdate() - 如果用了
onBeforeUpdate()这类命名方法,必须是public static,且参数固定为$model(不是$query) - 多个事件共存时,
init()里注册优先级高于静态钩子;冲突时以后者为准
beforeWrite 和 beforeInsert/beforeUpdate 的触发顺序容易混淆
beforeWrite 是最外层写入拦截点,在 save() 开始执行时立刻触发,不管当前是新增还是更新;而 beforeInsert 和 beforeUpdate 是派生事件,仅在明确判断出动作类型后才触发。
- 当
$model->id为空或为null,且未显式设置isUpdate(true),则走插入流程 → 触发beforeWrite→beforeInsert - 当
$model->id存在,或调用了isUpdate(true),则走更新流程 → 触发beforeWrite→beforeUpdate -
beforeWrite里不能依赖$model->id是否存在来判断类型,因为此时可能还没做主键检测 - 需要区分逻辑时,优先用
beforeInsert/beforeUpdate,而非在beforeWrite里手动 if 分支
异步同步场景下 afterSave 不等于“事务已提交”
在 afterSave 或 afterUpdate 里直接调用 HTTP 请求同步 ES、发消息,极大概率导致数据不一致——因为模型事件在事务 commit 前就已触发,而事务中途 rollback 时,外部系统已收到脏数据。
立即学习“PHP免费学习笔记(深入)”;
- 真正安全的做法是监听
Db::event('after_commit')(数据库层事务提交后),但该事件需配合手动开启事务使用 - 更稳妥的是把同步逻辑推到队列,例如在
afterUpdate中只推送任务 ID 到 Redis 或 RabbitMQ - 不要在事件回调里做耗时操作,否则阻塞主请求;ThinkPHP 默认事件是同步执行,无自动异步封装
- 若用 Swoole 环境,注意事件回调可能跨协程,
$model实例状态不可靠,应只取关键字段如$model->id、$model->toArray()
模型事件的边界很清晰:它只响应模型方法调用,不响应原生 Db::table()->update() 或原生 SQL。想统一拦截所有写操作,得退到数据库事件层,用 Db::event('before_update', ...) ——但那样就失去了模型上下文和关联关系。



















