项目里有一张用户表,每次新增用户的时候,都要手动写状态、来源渠道、最后登录时间这些字段。一开始还能忍,后来越加越多,搞得新增用户的代码里全是setxxx。后来看到同事在模型里写了个onAfterInsert,才意识到ThinkPHP8的模型事件能把这些重复逻辑统统收编。
这篇文章不聊虚的,直接拿三个实际场景来拆解:自动填充字段、记录操作日志、更新缓存。看完你也能把模型事件用起来。
先搞清楚模型事件在ThinkPHP8里怎么玩
ThinkPHP8的模型事件其实就是一组在模型操作时自动调用的方法。你不需要手动触发,框架会在新增、更新、删除等操作的前后自动调用。常见的事件有这些:
onBeforeInsert 新增前
onAfterInsert 新增后
onBeforeUpdate 更新前
onAfterUpdate 更新后
onBeforeDelete 删除前
onAfterDelete 删除后
onBeforeWrite 写入前(含新增和更新)
onAfterWrite 写入后
定义方式很简单,在模型类里写一个名为onAfterInsert之类的静态方法就行。比如:
class User extends Model
{
public static function onAfterInsert(User $model)
{
// 新增后要做的逻辑
}
}
方法接收当前模型实例,通过$model->getData()可以拿到所有字段数据。注意这个User是模型类名,方法参数用User $model是为了类型提示,写不写都行。
案例一:自动填充字段,新增用户不再重复劳动
假设在新增用户时,必定要生成一个用户编号,同时设置默认头像和用户状态。原来写控制器的时候可能这样:
$user = new User();
$user->username = $data['username'];
$user->status = 1;
$user->avatar = '/default.png';
$user->userno = date('YmdHis') . rand(1000, 9999);
$user->save();
每个新增的地方都要写这三行,万一以后默认头像换了,还得全局搜。现在全部收进模型事件里:
class User extends Model
{
public static function onBeforeInsert(User $model)
{
// 自动填充字段
$model->status = 1;
$model->avatar = '/default.png';
$model->userno = date('YmdHis') . rand(1000, 9999);
}
}
控制器里只保留业务字段:
$user = User::create($data);
这样不管哪里新增用户,这些默认字段都会被自动加上。如果你也想在更新前自动修改某个字段,类似地写onBeforeUpdate即可。
案例二:记录操作日志,不用在业务代码里埋雷
很多时候需要记录谁在什么时候改了什么数据。最常见的做法是在更新用户信息的代码里手动写日志逻辑,但很容易漏。模型事件可以统一拦截。
例如我要记录用户新增、更新、删除的操作日志,先创建一个日志表模型UserLog:
class UserLog extends Model
{
protected $name = 'user_log';
protected $autoWriteTimestamp = true;
}
然后在User模型中写三个事件:
class User extends Model
{
public static function onAfterInsert(User $model)
{
self::writeLog($model, '新增用户');
}
public static function onAfterUpdate(User $model)
{
self::writeLog($model, '更新用户');
}
public static function onAfterDelete(User $model)
{
self::writeLog($model, '删除用户');
}
protected static function writeLog(User $model, string $action)
{
// 排除一些不需要记录的字段
$data = $model->getData();
unset($data['password']);
UserLog::create([
'user_id' => $model->id ?? 0,
'action' => $action,
'data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'ip' => request()->ip(),
'time' => time(),
]);
}
}
这里要注意的是,删除操作之后$model->id仍然存在,因为数据已经查出来了。如果你的模型是软删除,事件同样会触发,这点后面再说。
这样业务代码里只需要调用$user->save()或$user->delete(),日志自动留下。无论哪里操作了用户表,你都能从日志中追溯。
案例三:缓存联动,更新完数据立刻清掉旧缓存
缓存是提升性能的利器,但缓存一致性很头疼。以前我习惯在更新数据后手动删除缓存:
$user = User::find(1);
$user->nickname = '新名字';
$user->save();
cache('user_info_' . $user->id, null);
这个cache(... null)删缓存操作很容易漏。有了模型事件,就可以把它固定在onAfterWrite事件里,因为写操作包括新增和更新:
public static function onAfterWrite(User $model)
{
// 如果存在这个缓存键,就删掉
if (cache('user_info_' . $model->id)) {
cache('user_info_' . $model->id, null);
}
}
其他地方读取缓存的方法不变:
$userInfo = cache('user_info_' . $id);
if (!$userInfo) {
$userInfo = User::find($id)->toArray();
cache('user_info_' . $id, $userInfo, 3600);
}
因为模型事件在写成功后自动清缓存,所以任何入口只要修改了用户信息,下次读取就会自动回源,不需要在每个修改的地方手动清。
还有一个进阶玩法。你可能希望只有在某些字段变化时才清缓存,比如余额和积分,不需要每次昵称变化都清。可以在事件里判断模型的修改字段:
public static function onAfterUpdate(User $model)
{
$changed = $model->getChangedData();
if (isset($changed['money'])) {
cache('user_money_' . $model->id, null);
}
}
getChangedData() 是ThinkPHP8提供的方法,能拿到本次更新中实际被修改的字段值。这样缓存清理更精细,也更高效。
再聊聊模型事件和模型观察者的区别
ThinkPHP8还提供了观察者(Observer)机制。简单说,观察者是一个单独的类,专门用来监听模型事件。适合一个模型有很多事件逻辑,或者多个模型共用一套事件逻辑。
定义一个观察者类:
class UserObserver
{
public function afterInsert(User $user)
{
// 新增后逻辑
}
public function afterUpdate(User $user)
{
// 更新后逻辑
}
}
然后在模型里声明:
class User extends Model
{
protected $observer = UserObserver::class;
}
或者全局注册。我自己习惯把日志和缓存这种横切逻辑放在观察者里,而把字段填充这种跟模型强相关的事件放在模型里。看你项目结构,没有绝对对错。
几个容易踩的坑
模型事件虽然方便,但用不好也会出麻烦。下面这几个坑我都踩过,写出来让你避一下。
第一个坑:事件里调用save()会无限递归。比如你在onAfterInsert里又调用了$model->save(),那会再次触发插入事件,循环下去。所以事件里尽量不要去操作当前模型,除非你明确知道自己在干嘛。
第二个坑:用create()方法时同样会触发事件。很多新手不知道User::create($data)和$user->save()其实都会触发模型事件,这是好事,但有时候你只想简单插入一行,不想触发事件,那需要把事件临时关闭。ThinkPHP8提供了Model::unlinkEvent()静态方法:
Model::unlinkEvent('before_insert');
User::create($data);
Model::linkEvent('before_insert');
记得操作完恢复,不然后续模型的事件都不执行了。
第三个坑:软删除也会触发删除事件。如果你使用SoftDelete特性,执行delete()时实际上执行的是更新操作,但模型事件还是按delete来触发。也就是说onAfterDelete会被调用,但onAfterUpdate不会被调用。别搞混,不然日志里记录的内容可能不对。
总结一下模型事件给我带来的好处
模型事件最大的价值,就是让那些跟数据本身相关的横切逻辑不再散落在各个控制器里。字段填充、日志记录、缓存清理,这些都是对数据操作的“副作用”,放在模型这一层再合适不过。
前阵子我把项目里所有新增/更新用户的地方都清了一遍,删掉了大量重复代码,只保留业务独有的逻辑。现在新增一个用户接口,只需要五行代码,剩下的交给模型事件去处理。如果以后需要改变用户编号规则或者缓存策略,只需要改一个地方,别提多舒服了。
建议你先挑一个简单的模型试试,从字段填充开始,感受一下模型事件的魔力。然后再慢慢加上日志和缓存,相信你会回来感谢我的。

