ThinkPHP6 事件机制我挂错顺序:一个 `AppInit` 里抛异常导致全站服务注册中断的深夜
上周给项目加支付回调的日志追踪,想着用事件解耦,在 `AppInit` 里注册了一个全局监听器,结果测试环境一部署,整个后台登录页直接白屏。报错信息特别客气:"Undefined variable: config",堆栈指向框架内核某个我从来没打开过的文件。
查了四十分钟才反应过来:我那个监听器里调了某个 service,而那个 service 的构造函数里用了 `config()` 助手函数——但 `AppInit` 触发的时候,配置加载还没走完。框架生命周期这玩意儿,顺序错了就是致命伤。
后来我把常用挂载点按"能干什么、不能干什么"重新理了一遍,贴这儿供参考,都是踩出来的:
`AppInit`:最早,也最危险
这时候框架刚实例化,`$app` 有了,但配置、路由、数据库全没准备好。适合做什么?注册容器绑定、定义常量、改默认路径。我现在的规矩是:`AppInit` 里只写 `bind()` 和 `env()` 读取,绝不碰业务逻辑。那次支付回调的监听器,后来挪到了 `HttpRun`,一切正常。
`HttpRun`:请求级,但还没路由
每次 HTTP 请求进来都会走。这里可以读配置了,也能操作请求对象,但路由解析还没开始。我通常在这里做全局的请求头注入、跨域预检处理。有个坑:如果你在 `HttpRun` 里 `exit` 或者抛未捕获异常,会直接中断,不会走到后面的中间件。
`RouteLoaded`:路由表已定,URL 还没匹配
动态注册路由的最后机会。我做过一个多站点插件系统,不同域名加载不同路由组,就是在这里根据 `$_SERVER['HTTP_HOST']` 判断后 `Route::group()`。注意这时候 `request()->url()` 还不准,得用原生 server 变量。
`ControllerBegin` / `ControllerEnd`:业务层的钩子
控制器执行前后。我主要用来打性能标记,或者做权限的兜底校验。但说实话,能放中间件的我不放这里,因为事件监听器的执行顺序不如中间件直观,调试起来费劲。
调试这些钩子的土办法
事件最大的坑是"静默"——挂了不报错,或者报错位置离实际原因八丈远。我现在每个监听器开头必加一行:
trace_log('event_'.__METHOD__, ['file'=>__FILE__, 'line'=>__LINE__]);
不是用框架的 `trace`,是自己写的一个简单文件日志,专门记事件流转。另外 `php think event:list` 这个命令要常用,能看当前注册了哪些监听器,但注意它只显示"通过配置文件或标签注册的",代码里动态 `Event::listen()` 的不显示。
还有一个阴间 bug:如果你用 `Event::trigger()` 触发自定义事件,但监听器类名写错了,ThinkPHP6 默认不抛异常,直接当没注册处理。我现在的习惯是触发前加 `Event::hasListeners('xxx')` 断言,或者干脆在监听器构造函数里故意抛个测试异常,确认链路通不通。
那次 `AppInit` 事故之后,我在项目文档里加了一条:所有事件监听器 PR 必须附生命周期时序图,手写也行。听起来形式主义,但真的,这玩意儿顺序错了,排查成本比逻辑 bug 高十倍。
你们一般怎么挂钩子的?有没有在 `LogWrite` 或者 `AppEnd` 里埋过什么骚操作?