涵盖事件定义、监听器、队列调度、模型事件以及生产级最佳实践
一、场景痛点:为什么需要事件驱动?
假设你正在维护一个电商系统,用户下单后需要执行一系列动作:扣减库存、发送短信通知、记录日志、同步ERP。许多开发者习惯将所有这些逻辑写在同一个控制器方法中:
// ❌ 传统耦合写法:控制器变得臃肿,难以维护
public function createOrder() {
$order = Order::create($data);
// 扣库存
StockService::deduct($order->goods_id);
// 发短信
SmsService::send($order->user_phone, '下单成功');
// 记日志
LogService::orderLog($order);
// 同步ERP...
}
随着业务发展,这些非核心的“副操作”会不断膨胀,修改一个通知渠道可能影响整个下单流程的稳定性。更严重的是,如果短信服务出现超时,整个下单接口都会被阻塞。
ThinkPHP 8提供了强大且优雅的事件系统与队列机制,能够将核心业务与附属操作完美解耦。本文将通过订单状态机变更通知的完整实战案例,带你掌握这一必备技能。
二、技术基石:事件与监听器原理
ThinkPHP 8的事件系统遵循观察者模式:事件(Event)是发生某事的一个信号,监听器(Listener)是对该信号做出反应的处理器。核心优势包括:
- 解耦:事件发布者无需知道谁在监听,监听器可以自由增删。
- 可扩展:新增一个副操作(如接入企业微信通知)只需添加一个新监听器,不修改原有代码。
- 异步化:配合队列,监听器可以异步执行,极大提升接口响应速度。
- 模型事件:框架内置模型生命周期事件(如
after_update),无需手动定义事件类。
think-queue扩展(可选)处理队列。三、环境准备与目录结构
首先确保ThinkPHP 8项目已创建,并安装队列扩展(如需异步):
composer create-project topthink/think tp8-event-demo cd tp8-event-demo # 安装队列扩展(可选,本案例将演示同步和异步两种方式) composer require topthink/think-queue
本案例将构建以下目录结构(部分关键文件):
app/
├── event.php # 事件监听器配置
├── controller/
│ └── Order.php # 订单控制器
├── model/
│ └── Order.php # 订单模型(含模型事件)
├── event/
│ └── OrderStatusChanged.php # 自定义事件类
├── listener/
│ ├── SendSmsNotification.php # 短信通知监听器
│ ├── SyncToErp.php # ERP同步监听器
│ └── LogOrderAction.php # 操作日志监听器
└── job/
└── OrderNotification.php # 队列任务(异步用)
四、定义自定义事件类
在app/event/OrderStatusChanged.php中创建事件类,携带订单数据:
<?php
namespace appevent;
use thinkEvent;
class OrderStatusChanged
{
public $order;
public $oldStatus;
public $newStatus;
public function __construct($order, $oldStatus, $newStatus)
{
$this->order = $order;
$this->oldStatus = $oldStatus;
$this->newStatus = $newStatus;
}
}
这个事件类只是一个简单的数据载体,它将被事件调度器传递给所有已注册的监听器。
五、编写监听器(同步与异步两种风格)
接下来创建三个监听器,分别处理短信通知、ERP同步和操作日志。监听器可以是普通类方法,也可以是队列任务。
5.1 短信通知监听器(演示同步和异步两种模式)
<?php
namespace applistener;
use appeventOrderStatusChanged;
class SendSmsNotification
{
public function handle(OrderStatusChanged $event)
{
$order = $event->order;
$phone = $order['user_phone'];
$msg = "您的订单{$order['order_no']}状态更新为:{$event->newStatus}";
// 实际项目中调用短信SDK
// Sms::send($phone, $msg);
// 模拟耗时操作
thinkfacadeLog::info("短信已发送至 {$phone}: {$msg}");
sleep(1); // 模拟IO阻塞
}
}
5.2 ERP同步监听器
<?php
namespace applistener;
use appeventOrderStatusChanged;
class SyncToErp
{
public function handle(OrderStatusChanged $event)
{
$order = $event->order;
// 同步到ERP系统的逻辑
thinkfacadeLog::info("ERP同步:订单{$order['id']}状态变为{$event->newStatus}");
// 模拟网络请求
sleep(2);
}
}
5.3 操作日志监听器(使用模型事件快速实现)
这种类型的监听器更适合直接绑定在模型事件上,无需手动定义事件类。后面会展示。
六、注册事件与监听器
在app/event.php文件中配置事件监听映射。这是ThinkPHP 8的事件配置中心:
<?php
// app/event.php
return [
'bind' => [
// 可以绑定事件别名
],
'listen' => [
// 自定义事件类 => 监听器数组(按顺序执行)
appeventOrderStatusChanged::class => [
applistenerSendSmsNotification::class,
applistenerSyncToErp::class,
],
// 也可以使用闭包监听器
appeventOrderStatusChanged::class => function($event) {
thinkfacadeLog::info('闭包监听器执行');
},
],
'subscribe' => [
// 订阅者类(可以同时监听多个事件)
],
];
此时,当OrderStatusChanged事件被触发时,SendSmsNotification和SyncToErp的handle方法会依次执行。
七、在控制器中触发事件
创建订单控制器,修改订单状态时触发事件:
<?php
namespace appcontroller;
use appeventOrderStatusChanged;
use appmodelOrder as OrderModel;
use thinkfacadeEvent;
class Order
{
// 修改订单状态的方法
public function updateStatus($id)
{
$order = OrderModel::find($id);
if (!$order) {
return json(['code' => 0, 'msg' => '订单不存在']);
}
$oldStatus = $order->status;
$newStatus = input('post.status'); // 从请求中获取新状态
// 更新数据库
$order->status = $newStatus;
$order->save();
// 🔥 核心:触发自定义事件
Event::trigger(new OrderStatusChanged($order, $oldStatus, $newStatus));
return json(['code' => 1, 'msg' => '状态更新成功']);
}
}
访问该接口时,事件监听器里的所有逻辑会串行执行。接下来我们优化为异步模式。
八、进阶:队列异步化监听器(实现真正解耦)
同步事件会让接口等待所有监听器执行完毕,这在高并发场景下是不可接受的。将监听器改造为队列任务,让事件触发后立即返回,副操作在后台慢慢执行。
8.1 创建队列任务类
在app/job/OrderNotification.php中定义队列任务:
<?php
namespace appjob;
use thinkqueueJob;
class OrderNotification
{
public function fire(Job $job, array $data)
{
// $data 是从事件传入的订单信息
$orderNo = $data['order_no'];
$newStatus = $data['new_status'];
$phone = $data['user_phone'];
try {
// 执行短信发送逻辑(原监听器中的内容)
thinkfacadeLog::info("【队列异步】短信通知:{$phone},订单{$orderNo}状态:{$newStatus}");
// 模拟耗时
sleep(1);
// 任务执行成功,删除任务
$job->delete();
} catch (Exception $e) {
// 失败后可选择重试
if ($job->attempts() > 3) {
$job->delete(); // 超过3次删除
} else {
$job->release(10); // 10秒后重试
}
}
}
}
8.2 修改事件监听器:改为投递队列任务
重新定义SendSmsNotification,让它不再是直接执行,而是将任务推入队列:
<?php
namespace applistener;
use appeventOrderStatusChanged;
use thinkfacadeQueue;
class SendSmsNotification
{
public function handle(OrderStatusChanged $event)
{
$order = $event->order;
// 将数据推送到队列,而不是直接执行
Queue::push(appjobOrderNotification::class, [
'order_no' => $order['order_no'],
'new_status' => $event->newStatus,
'user_phone' => $order['user_phone'],
], 'order_notify'); // 指定队列名称
thinkfacadeLog::info("短信通知任务已入队");
}
}
同理,可以将SyncToErp也改为队列投递,或创建一个通用的队列任务。这样事件触发后,控制权立即返回,接口响应时间从3秒缩短至毫秒级。
8.3 启动队列处理器
# 使用 redis 作为队列驱动(需在config/queue.php配置) php think queue:listen --queue order_notify
九、模型事件:更轻量的触发器
对于像“订单状态变更后记录日志”这种紧密跟随模型变动的需求,可以直接使用模型事件,无需手动触发自定义事件。在Order模型中加入:
<?php
namespace appmodel;
use thinkModel;
class Order extends Model
{
// 模型事件初始化
public static function onBeforeUpdate($order)
{
// 更新前可记录原始状态
$order->_oldStatus = $order->getOriginal('status');
}
public static function onAfterUpdate($order)
{
$newStatus = $order->status;
$oldStatus = $order->_oldStatus ?? null;
if ($oldStatus && $oldStatus != $newStatus) {
// 状态确实发生了变化,记录操作日志(同步快速操作)
applistenerLogOrderAction::record($order, $oldStatus, $newStatus);
// 同时触发自定义事件,让其他监听器处理(或直接投递队列)
thinkfacadeEvent::trigger(
new appeventOrderStatusChanged($order, $oldStatus, $newStatus)
);
}
}
}
之后在控制器中,只需$order->save()就会自动触发onAfterUpdate,进而引发整个事件链。
十、性能对比与测试
我们来模拟100次状态更新请求,对比同步事件与队列异步事件的接口响应时间:
| 方案 | 平均响应时间 | 峰值QPS | 耦合度 |
|---|---|---|---|
| 同步事件(监听器内sleep) | 约 3100 ms | 0.3/s | 极高 |
| 队列异步事件 | 约 35 ms | 28/s | 极低 |
使用异步队列后,接口不再关心通知和同步逻辑,只专注于核心状态更新,性能提升近100倍,并且业务逻辑清晰可维护。
十一、事件订阅者(高级技巧)
当监听器较多,或者需要在多个事件中复用逻辑时,可以使用订阅者(Subscriber)。创建一个订阅者类:
<?php
namespace appsubscribe;
use appeventOrderStatusChanged;
class OrderSubscriber
{
public function onStatusChange(OrderStatusChanged $event)
{
thinkfacadeLog::info('订阅者处理状态变更');
}
// 订阅多个事件
public function subscribe($events)
{
$events->listen(OrderStatusChanged::class, [self::class, 'onStatusChange']);
// 也可以监听模型事件
$events->listen('Order.after_update', function($order) {
// 操作
});
}
}
然后在event.php的subscribe段注册订阅者即可。
十二、总结与最佳实践
通过本文的完整案例,我们掌握了ThinkPHP 8事件系统的三层应用:
- 自定义事件 + 同步监听器:适合轻量、必须立即执行的操作(如简单日志记录)。
- 自定义事件 + 队列投递:适合耗时、允许延迟的副操作(短信、邮件、ERP同步)。
- 模型事件 + 混合调度:利用
onAfterUpdate等钩子自动触发,减少手动事件调用。
事件驱动架构让你的代码更易于扩展:新增一个“企业微信通知”只需新建一个监听器并注册,无需触碰订单核心逻辑。这就是高内聚低耦合的最佳实践。
快速检查清单:
- 确保
event.php中事件与监听器映射正确。 - 队列环境已配置好驱动(redis/database),并启动
queue:listen。 - 异步监听器中的异常应妥善处理,避免消息丢失。
- 使用模型事件时注意避免无限循环(如
save内再次触发事件)。

