模型事件方法不能直接依赖注入,因其为静态方法且不走容器实例化流程;需提前绑定服务到容器,并在事件中用app()显式获取。

模型事件方法里不能直接依赖注入
TP6 的模型事件(如 onBeforeWrite、onAfterWrite)是静态方法,由框架通过反射调用,**不走容器实例化流程**,所以构造函数注入、属性注入、或方法参数注入全部无效。你写 public function onBeforeWrite(User $user) 会直接报错或参数为 null。
常见错误现象:
– 事件方法里调用 $this->app 报错「Undefined property」
– 用 app('xxx') 拿不到预期对象(比如自定义服务类没绑定)
– 试图在静态方法里用 $this 访问非静态上下文
- 根本原因:静态事件回调 ≠ 控制器/中间件/命令行类,它没有被容器管理,也不经过依赖解析
- 正确思路:把需要的服务提前绑定到容器,再在事件方法里用
app()或think\Container::get()显式获取 - 必须确保该服务已注册(在
provider.php、服务提供者register()方法中,或启动时手动bind())
如何在 onAfterWrite 里安全调用自定义日志服务
假设你写了一个审计日志服务 app\service\AuditService,想在 onAfterWrite 中复用它的 record() 方法,而不是重复写 Db::name('audit_log')->insert(...)。
第一步:绑定服务到容器(推荐在 app/provider.php 中):
return [
'audit' => \app\service\AuditService::class,
];
第二步:在模型事件方法中显式获取并调用:
protected static function onAfterWrite($model)
{
$audit = app('audit'); // ✅ 正确:从容器取已绑定服务
$audit->record([
'action' => $model->isUpdate() ? 'update' : 'insert',
'table' => $model->getTable(),
'record_id' => $model->id,
'user_id' => session('user_id') ?: 0,
'ip' => request()->ip(),
'changes' => $model->isUpdate() ? json_encode(array_diff_assoc($model->getData(), session('audit_snapshot') ?: []), JSON_UNESCAPED_UNICODE) : null,
]);
session('audit_snapshot', null);
}
- 不要用
new AuditService():绕过容器,无法享受依赖注入(比如它内部依赖Request或Db就会失败) - 不要用
app(\app\service\AuditService::class):虽然可行,但耦合类名,不利于后期替换实现 - 如果服务有构造参数(如需传入配置),必须用闭包绑定方式,例如:
app()->bind('audit', function () { return new AuditService(config('audit')); });
为什么 bind('xxx', XxxClass::class) 和 bind('xxx', XxxClass::class, true) 效果不同
第三个参数 $shared 决定是否单例。默认 true,即每次 app('xxx') 返回同一个实例;设为 false 则每次返回新实例。
对模型事件场景的影响很实际:
- 日志服务、缓存服务、HTTP 客户端等——应设为
true(单例),避免重复初始化连接或资源 - 如果你的服务内部维护了请求上下文状态(比如临时存储当前 model 实例),又希望每次事件调用都干净隔离——才考虑
false,但极少见,且容易引发不可预测行为 - 漏掉这个参数导致性能问题:比如每次
onAfterWrite都新建一个数据库连接池实例
所以明确写出来更安全:app()->bind('audit', \app\service\AuditService::class, true);
事件方法里调用容器服务的兼容性注意点
TP6.0+ 版本中,app() 助手函数在模型事件中可用,但要注意执行时机:
- 事件触发时,应用已初始化完成,
app()可用;但request()、session()等门面函数依赖当前请求上下文,在 CLI 场景(如命令行导入数据)可能为空或抛异常 - 若审计逻辑要兼容 CLI,别直接调
request()->ip(),先判断:if (app()->isCli()) { $ip = '127.0.0.1'; } else { $ip = request()->ip(); } -
session()在 CLI 下默认不可用,需手动启用或改用其他存储(如Cache::store('file')->set()) - 所有容器服务必须在事件触发前完成绑定——放在
provider.php最稳妥;在控制器里bind()是无效的,模型事件不会感知
最易被忽略的一点:模型事件是静态上下文,而容器服务可能是有状态的(比如带私有属性的 logger 实例)。确保你的服务类本身是无状态设计,或状态只与当前调用相关,否则多个并发写入可能互相污染。











