很多人在一个控制器里写完了所有业务,比如下单之后要发短信、写日志、更新库存、赠送积分。一开始没啥,后来越加越多,一个方法几百行,稍微改动一个通知就感觉整个下单流程被动。前阵子我用ThinkPHP 8重写一个订单模块,用事件系统把“下单后的事”全部拆了出去,现在主流程干干净净。
事件是什么
事件就是“发生了一件事”。ThinkPHP的事件系统允许你在业务代码里“触发”一个事件,然后由监听器去响应。业务方不关心谁去响应,响应者也不用打扰主流程。这就是解耦。
第一步:定义事件类
在ThinkPHP 8里,事件类就是普通的类,用来携带数据。我建了一个 appeventOrderCreated 事件类。
<?php
namespace appevent;
class OrderCreated
{
public $order;
public $user;
public function __construct($order, $user)
{
$this->order = $order;
$this->user = $user;
}
}
构造函数接收订单数据和用户数据。谁触发事件,谁负责传参。
第二步:注册事件与监听器
ThinkPHP的事件可以通过 config/event.php 文件定义。我在 event 数组里加上事件和监听器的映射。
<?php
return [
'bind' => [
'OrderCreated' => 'appeventOrderCreated',
],
'listen' => [
'appeventOrderCreated' => [
'applistenerSendSmsListener',
'applistenerWriteLogListener',
'applistenerUpdateInventoryListener',
],
],
];
意思是当触发 appeventOrderCreated 事件时,依次执行这三个监听器。
第三步:编写监听器
监听器需要实现 handle 方法。比如发送短信的监听器:
<?php
namespace applistener;
class SendSmsListener
{
public function handle($event)
{
// 调用短信服务,使用事件中携带的数据
$orderNo = $event->order['order_no'];
$phone = $event->user['mobile'];
// 假装发了一条短信
// SmsService::send($phone, "您的订单 {$orderNo} 已确认");
trace("发送短信给 {$phone},订单号:{$orderNo}");
}
}
日志监听器、库存监听器同理。每个监听器只管自己那一摊事。
第四步:在业务中触发事件
下订单的控制器方法变得非常简单:
<?php
namespace appcontroller;
use thinkfacadeEvent;
use appeventOrderCreated;
class Order
{
public function create()
{
// 创建订单主记录
$order = ...;
// 如果有其他必要操作,写在这里
// 触发订单创建完成事件
Event::trigger(new OrderCreated($order, $user));
return json(['code' => 1, 'msg' => '下单成功']);
}
}
如果你后续要加一个“下单后给渠道商返佣”,不用改控制器,只需要新建一个监听器,然后在 event.php 里注册进去即可。
第五步:异步监听(队列)
短信和日志其实不一定要同步执行。ThinkPHP事件支持将监听器设置为异步,借助消息队列。我当初把发短信的监听器丢到了队列里,接口秒回。
首先在 event.php 里设置监听器为队列模式:
'listen' => [
'appeventOrderCreated' => [
'applistenerSendSmsListener' => 'queue',
'applistenerWriteLogListener',
...
],
],
然后监听器的 handle 方法需要返回一个队列任务或者使用ShouldQueue? 不过ThinkPHP 8里通常通过thinkqueueListener来消费。这个看业务情况,如果是核心依赖的话建议同步,非核心异步。
踩坑记录
1. 事件类被 Event::trigger 触发时,监听器的 handle 参数是事件对象,不是事件名。
2. 在event.php中注册监听器时,一定要用完整命名空间,别用短别名,否则系统找不到。
3. 如果你在监听器里修改了 $event 属性的值,其实不会影响回传,因为事件对象是引用传递,但如果你重新赋值了监听器里的 $event,不会影响到外部。更多时候我们需要在主流程里获取结果,可以用Event::until获取第一个非null的返回。
更进一步:事件监听还可以用注解注册
如果你不喜欢配置文件,想直接用属性注解(PHP8)注册监听器,ThinkPHP 8也支持。比如在监听器类上写#[Listen('appeventOrderCreated')],然后通过命令行扫描生成事件。个人还是喜欢事件文件集中管理,容易找。
总结
事件系统不是复杂概念,就是“多了一个回调列表”。用了它之后,我的下单方法保留了核心逻辑,所有扩展功能都扔给了监听器。代码好读多了,也好测试了。下次添加新功能,真的就是加一个监听器的事。

