ThinkPHP6 事件订阅里我写了三个 `listen`,结果只有第一个触发,排查三小时发现是 `app/Event.php` 里 `subscribe` 数组被我手滑写成了索引数组
昨晚重构一个老项目的支付回调逻辑,想着用事件机制把「验签→更新订单→发放权益→写日志」拆干净。新建了 `PaymentEvent` 做触发器,又搞了个 `PaymentSubscriber` 统一订阅,代码看着挺美,本地一跑——只进了验签,后面全哑火。
断点跟到 `Event::trigger()`,三个 `listen` 方法明明都注册了,调度队列里却只有第一个监听器。翻官方文档没看出毛病,最后盯着 `config/event.php` 看了十分钟,发现 `subscribe` 数组里我写的:
'subscribe' => [
'app\subscribe\PaymentSubscriber',
'app\subscribe\SmsSubscriber'
]
问题不在格式,是我另一个项目复制过来的配置里,`subscribe` 被前任写成了:
'subscribe' => [
0 => 'app\subscribe\PaymentSubscriber',
'sms' => 'app\subscribe\SmsSubscriber'
]
ThinkPHP 的 `Event` 类里 `subscribe` 遍历用的是 `foreach`,索引数组和关联数组都能走,但我在 `PaymentSubscriber` 的 `subscribe` 方法里,为了"优雅"用了 `return [ 'payment.success' => ['onPaymentSuccess'], ... ]` 这种键值对形式。框架解析时把键当成事件名,值当成回调,而我第一个 `listen` 方法名恰好和事件名对上了,后面两个键值对因为数组结构问题被吞了。
更准确说,是我混用了两种订阅写法:类级别的 `subscribe` 方法返回映射数组,和直接用 `Listener` 注解。同一个订阅者里,框架优先走 `subscribe` 方法,方法里返回的数组如果结构不对,后面的 `listen` 方法压根不会被反射扫描。
说人话就是:你想用 `listen` 方法一个个挂,就别写 `subscribe` 方法;你想用 `subscribe` 方法批量映射,就别指望框架再帮你自动扫描 `listen`。 两者共存时,`subscribe` 方法的返回值会覆盖自动扫描的结果,而我那个返回值只写了一组映射。
改完这处,三个回调顺序执行,但又发现 `onPaymentSuccess` 里抛异常会导致后面两个 `listen` 中断。查了下源码,`trigger` 默认是同步顺序执行,没有 `try-catch` 兜底。现在我的做法是:每个 `listen` 方法内部自己吞异常写日志,或者把非核心操作(发通知、记日志)改成异步队列,主链路只保留订单状态变更。
另外分享个调试技巧:在 `vendor/topthink/framework/src/think/Event.php` 的 `trigger` 方法里临时 `dump($this->listener)`,能看到当前事件名下到底挂了哪些回调,比 `trace` 日志直观。生产环境别这么干,本地调生命周期的时候很省时间。
最后列下我现在的挂事件原则,踩过坑的应该能共鸣:
1. AppInit 里只干注册,不执行业务——之前图省事在里面连了 Redis,结果 Redis 挂了整站起不来;
2. HttpRun 之后再用 request 相关的东西——`AppInit` 阶段 `request` 还没实例化,取 IP、取路由全是空的;
3. 事件名用常量类管理——字符串拼写错一个字母,调试成本翻倍,我现在是 `EventName::PAYMENT_SUCCESS` 这种写法;
4. 订阅者里加 `priority` 注解控制顺序——虽然框架默认按注册顺序走,但多个订阅者挂同一个事件时,优先级显式写明能避免"谁先到谁说了算"的玄学。
这事件机制用顺了确实比直接在 Controller 里堆逻辑清爽,但框架给的自由度太高,边界不清的时候坑也深。你们一般怎么平衡"拆太细"和"调不动"的?我现在的底线是:一个事件链路超过四个监听,就考虑拆队列或者上 Saga 补偿了。