模型事件不触发,八成是没注册、没启用或名字写错了——不是逻辑问题,是配置和调用链断了。
先从最常见的坑说起。很多开发者以为写好了模型事件方法,事件就会自动触发。实际上,ThinkPHP并不会自动去扫描你的类里有哪些事件方法,它需要你明确地告诉它“哪个方法要在哪个时机执行”。也就是说,你必须老老实实到 initialize() 里去注册。

模型事件必须在 initialize() 里显式注册
就算你把方法名写得一字不差,只要没注册,就是白搭。在模型类中定义 public static function initialize(),然后在里面调用 self::event() 才是正道。
- 注册时,回调必须是数组形式,比如
self::event('beforeInsert', [UserModel::class, 'onBeforeInsert'])。别想着传闭包或者静态方法字符串,框架不认。 - 如果子类继承了基类模型,又重写了
initialize(),那你得手动加上一行parent::initialize(),不然父类注册的事件全会失效。这一点特别容易漏。 - 千万别在控制器或者服务层里写
UserModel::event(...),那是静态调用,并不会和实例生命周期绑定,事件跑不起来。
afterSelect 拿不到 SQL 是设计使然,得提前抓
很多人查问题时会跑到 afterSelect 里去取 SQL 日志,结果发现返回的是空的。别慌,这不是 Bug。当 afterSelect 触发时,查询早就执行完了,PDO 结果已经返回,原始 SQL 字符串已经被丢弃。
- 想记录完整的 SQL(连参数替换后的版本也要看),就必须在
beforeSelect阶段调用$query->buildSql(),把字符串拿到手,然后存到静态变量或者 Request 属性里。 $query->getLastSql()是在查询执行完后才生效的,放到afterSelect里调,大概率拿到的要么是空值,要么是上一次遗留下来的脏数据。- 用了
with()关联预加载?那每个关联表都是独立的Query实例,主模型的afterSelect根本管不到它们。所以想统一记录所有 SQL 的话,得额外想别的办法。
事件名必须小写且严格匹配标准列表
从 TP6.1+ 开始,框架已经废弃了所有驼峰、on 前缀、大写首字母之类的写法。现在只认以下小写命名——注意没有 on,没有大写字母:
created、updated、deleted、restoredbeforeWrite、afterWrite(注意:它在 insert 和 update 后都会触发,得靠$model->isUpdate()来区分)beforeInsert、afterInsert、beforeUpdate、afterUpdate- 软删除场景下,
forceDeleted才能捕获真正的物理删除delete(true),而deleted只响应软删(比如设置delete_time字段)。
全局事件系统开关 app.event 必须为 true
所有模型事件都依赖底层的事件驱动框架。如果 app.event 被设为 false,整个事件链就会静默中断,连 observe() 这种观察者模式都不工作。
- 首先去
config/app.php里确认'event' => true(TP6.3+ 已经移除了model.event,只看这个配置就好)。 - CLI 环境(比如执行命令行任务)经常因为缓存没有更新,或者加载了
app_cli.php导致该配置变成false。解决办法是运行一下php think config:clear,然后重启命令。 - 还有人习惯去
config/model.php里配event选项,注意,从 TP6.3 起这个配置项已经彻底失效了,别白费功夫。
最容易被忽略的一点:模型事件是内存阶段的钩子,并不是数据库事务的提交点。举个例子,sa ved 事件触发的时候,事务还没有 commit,外部服务是查不到这条记录的。这一点在做审计日志或者异步通知的时候,稍不注意就会导致数据不一致——做实时业务时尤其要留个心眼。