ThinkPHP6事件订阅实战:我把订单通知从"到处乱挂"重构到"一处监听"的踩坑记录
之前项目里的业务通知写得那叫一个随心所欲——控制器里直接调模型,模型里又塞邮件发送,支付回调里还硬编码了短信逻辑。上周改个通知文案,翻了五六个文件才找全,当场血压拉满。周末干脆把整个事件机制捋了一遍,重构完清爽多了,记录一下。
一、先分清:钩子、事件、中间件别再用混
刚开始我也蒙,ThinkPHP6里这几个概念长得太像。实际用下来我的理解是:
• 中间件:管请求进出的,偏HTTP层,比如鉴权、日志
• 事件(Event):业务状态变了要广播出去,谁爱听谁听
• 钩子(Hook):更老派的叫法,TP6里基本统一成事件系统了,但文档偶尔混用
我这次重构的核心就是把"订单状态变更"这件事抽成事件,至于要发邮件、推微信、写日志,那是监听者的事,订单模型只管抛事件。
二、注册监听:三种方式我踩了两个坑
TP6注册事件监听有三种路子,我全试了一遍:
1. 事件类+监听类(最规范,但我一开始写反了)
我傻乎乎把业务逻辑写进了Event类,后来才搞懂:Event类是"发生了啥",携带数据;Listener类才是"要干啥",处理逻辑。比如:
```php // app/event/OrderPaid.php —— 这是"消息信封" class OrderPaid { public $order; public function __construct($order) { $this->order = $order; } } // app/listener/SendSmsNotice.php —— 这是"处理人" class SendSmsNotice implements \think\event\ListenerInterface { public function handle($event) { // $event->order 拿数据 SmsService::send($event->order->mobile, '您的订单已支付...'); } } ```
2. 闭包监听(适合临时调试,生产慎用)
我在event.php配置里写过这个,方便是方便,但代码分散不好追踪:
```php Event::listen('OrderPaid', function($event) { Log::info('订单支付事件触发:' . $event->order->order_no); }); ```
3. 事件订阅器(最终采用的方案)
一个类管多个事件的监听,适合业务聚合。我的OrderSubscriber长这样:
```php class OrderSubscriber { public function subscribe(Event $event) { $event->listen('OrderPaid', [$this, 'onPaidSendSms']); $event->listen('OrderPaid', [$this, 'onPaidUpdateUserStats']); $event->listen('OrderShipped', [$this, 'onShippedNotifyExpress']); } public function onPaidSendSms($event) { ... } public function onPaidUpdateUserStats($event) { ... } } ```
然后在event.php的subscribe数组里挂上这个类,一处注册,到处生效。
三、调试事件:我加了这三招才定位到监听没触发
重构完测试,有个监听死活不执行,排查了半小时。总结几个调试技巧:
1. 先看事件有没有真的触发
控制器里用```Event::trigger('OrderPaid', new OrderPaid($order));```之后,立刻加一行Log::info确认执行到了。我那次问题就是前面有个return漏了,事件代码根本没跑到。
2. 监听类名大小写坑
TP6的事件名默认是字符串匹配,我配置里写的```'orderPaid'```,触发用的```'OrderPaid'```,Linux环境下直接不匹配。统一改成驼峰后解决,本地Windows反而不报错,坑。
3. 加个全局调试中间件
临时在event.php里塞了个万能监听,排查阶段很有用:
```php Event::listen('*', function($event, $params) { trace('事件触发:' . $event . ',参数类型:' . get_class($params[0] ?? new \stdClass())); }); ```
TP6不支持真正的通配符,这个'*'会被当成普通字符串事件名,但配合手动trigger调试用还行。更靠谱的是直接看runtime/log里的日志,或者Xdebug跟进去。
四、生命周期挂钩:我目前这么分层
现在项目里的事件挂载点,基本按这个节奏来:
| 阶段 | 事件示例 | 监听典型操作 |
|---|---|---|
| 数据写入前 | OrderCreating | 参数校验、防重复提交 |
| 数据写入后 | OrderCreated | 初始化关联数据 |
| 状态变更时 | OrderPaid / OrderShipped | 通知、统计、积分 |
| 异步队列 | OrderPaidQueue | 大任务延后处理 |
有个细节:支付回调这种第三方入口,我直接在回调控制器里trigger事件,然后立刻返回success,具体通知走队列异步发。之前同步发邮件,超时了微信重复通知三笔,血亏。
五、还没搞懂的点
文档里说事件可以走队列,但```Event::trigger```好像没有直接支持,我是手动在监听里```Queue::push```的。有没有老哥试过```event.php```里配置```'queue' => true```或者别的姿势?另外多个监听器的执行顺序,目前看是注册顺序,但有没有优先级控制我没找到,求指点。
重构完代码量其实多了,但控制器薄了一半,改需求不用翻七八个文件了。这周末打算把用户注册、退款也迁过来,希望别翻车。

