在大型应用中,业务逻辑往往会随着需求膨胀而变得臃肿。一个订单的创建可能同时触发短信通知、邮件发送、库存扣减、积分计算、操作日志记录等多个副作用。如果将这些代码全部写在控制器或模型中,类将迅速失去可维护性。ThoughtPHP 8 的 事件系统(Event System) 正是为了解决这一问题而生。它通过观察者模式将主业务与副作用解耦,允许你以声明式的方式定义“当某件事发生时,需要做哪些事情”。本文将带你从事件定义、监听器注册到订阅者模式,一步步构建一个完整的订单生命周期管理系统。
一、事件系统的核心概念
ThinkPHP 8 的事件系统由三个角色组成:
- 事件(Event):一个普通的 PHP 类,代表系统中发生的某个动作或状态变化。事件对象可以携带上下文数据(如订单ID、用户信息)。
- 监听器(Listener):一个具有
handle方法的类,当对应事件被派发时自动调用。一个事件可以绑定多个监听器。 - 订阅者(Subscriber):一个可以在单个类中处理多个不同事件监听的特殊类,适合将一组相关监听逻辑内聚在一起。
这种架构让你可以在不修改原有业务代码的情况下,随时添加新的副作用处理(如增加一个“发送企微通知”的监听器),完全符合开闭原则。
二、定义第一个事件:订单创建
首先,我们通过 artisan 命令快速生成事件类:
php think make:event OrderCreated
该命令会在 app/event/OrderCreated.php 生成一个基础事件类。我们补充构造函数来携带订单数据:
<?php
namespace appevent;
use appmodelOrder;
class OrderCreated
{
public function __construct(
public readonly Order $order,
public readonly int $userId
) {}
}
事件类本身是轻量数据传输对象(DTO),它描述了“发生了什么”以及相关的上下文信息。
三、创建监听器并绑定事件
接下来为 OrderCreated 事件创建三个监听器:发短信、加积分和记录日志。使用命令生成监听器:
php think make:listener SendSmsListener
php think make:listener AddPointsListener
php think make:listener LogOrderListener
以发送短信监听器为例,编写其 handle 方法:
<?php
namespace applistener;
use appeventOrderCreated;
class SendSmsListener
{
public function handle(OrderCreated $event): void
{
$order = $event->order;
$userId = $event->userId;
// 模拟发送短信通知
thinkfacadeLog::info("向用户{$userId}发送短信:订单{$order->id}已创建成功");
// 实际调用短信服务接口...
}
}
类似地,积分监听器和日志监听器分别处理对应逻辑。现在需要在 app/event.php 中注册事件与监听器的映射:
// app/event.php
return [
'bind' => [
appeventOrderCreated::class => [
applistenerSendSmsListener::class,
applistenerAddPointsListener::class,
applistenerLogOrderListener::class,
],
],
];
这种集中注册方式让事件流向一目了然。你也可以在监听器上使用 #[EventListener] 注解直接关联,但配置文件更适合集中管理。
四、在控制器中派发事件
当用户下单时,我们在订单服务中创建订单后立即派发事件。ThinkPHP 提供 Event::trigger() 或 event() 辅助函数:
<?php
namespace appcontroller;
use appmodelOrder;
use appeventOrderCreated;
use thinkfacadeEvent;
class OrderController
{
public function create()
{
// 假设已完成参数验证和订单数据写入
$order = Order::create([
'user_id' => session('user_id'),
'product_id' => input('product_id'),
'amount' => input('amount'),
'status' => 'pending',
]);
// 派发订单创建事件,所有监听器自动执行
Event::trigger(new OrderCreated($order, $order->user_id));
return json(['code' => 200, 'message' => '下单成功', 'order_id' => $order->id]);
}
}
事件派发后,控制器的职责到此结束。短信、积分、日志等操作在独立的监听器中异步或同步执行,主流程代码保持简洁且不受后续需求变更影响。
五、使用事件订阅者集中管理一组监听
如果多个事件的监听逻辑高度相关(如订单的所有状态变更都需要日志记录),可以用订阅者模式将多个 handle 聚合到一个类中,减少类文件数量并提升内聚性。
创建订阅者类 app/subscribe/OrderSubscriber.php:
<?php
namespace appsubscribe;
use appeventOrderCreated;
use appeventOrderPaid;
use appeventOrderShipped;
class OrderSubscriber
{
// 订单创建时的逻辑
public function onOrderCreated(OrderCreated $event)
{
thinkfacadeLog::info("订阅者记录:订单{$event->order->id}已创建");
}
// 订单支付时的逻辑
public function onOrderPaid(OrderPaid $event)
{
thinkfacadeLog::info("订阅者记录:订单{$event->order->id}已支付");
}
// 订单发货时的逻辑
public function onOrderShipped(OrderShipped $event)
{
thinkfacadeLog::info("订阅者记录:订单{$event->order->id}已发货");
}
// 这里可以继续添加其他事件处理方法...
/**
* 在 app/event.php 的 subscribe 键中注册该订阅者
*/
}
然后在 app/event.php 中添加 subscribe 配置:
return [
'bind' => [ /* 之前的绑定 */ ],
'subscribe' => [
appsubscribeOrderSubscriber::class,
],
];
订阅者类中每个以 on 开头的方法会自动映射到对应事件。当事件被触发时,订阅者内的方法与独立监听器同时执行,互不干扰。
六、实战:构建完整的订单生命周期事件链
现在我们将订单从创建到完成拆解为多个事件,形成一个完整的事件驱动链条。
6.1 定义所有订单事件
// OrderCreated.php - 携带订单和用户ID
// OrderPaid.php - 携带订单、支付方式和支付时间
// OrderShipped.php - 携带订单、快递公司和运单号
// OrderCompleted.php - 携带订单和完成时间
// OrderCancelled.php - 携带订单和取消原因
每个事件类的结构类似,但携带不同的上下文数据,确保监听器能获取到所需信息。
6.2 配置完整的事件绑定
// app/event.php
return [
'bind' => [
appeventOrderCreated::class => [
applistenerSendSmsListener::class, // 发送通知
applistenerAddPointsListener::class, // 赠送积分
applistenerReduceStockListener::class, // 扣减库存
],
appeventOrderPaid::class => [
applistenerSendReceiptListener::class, // 发送电子发票
applistenerUpdateSalesStatsListener::class,// 更新销售统计
],
appeventOrderShipped::class => [
applistenerSendShipNotifyListener::class, // 发货通知
],
appeventOrderCompleted::class => [
applistenerSendReviewInviteListener::class,// 邀请评价
applistenerUnlockAchievementListener::class,// 解锁成就
],
appeventOrderCancelled::class => [
applistenerRefundPointsListener::class, // 退还积分
applistenerRestoreStockListener::class, // 恢复库存
],
],
'subscribe' => [
appsubscribeOrderSubscriber::class, // 统一日志记录
],
];
6.3 控制器中触发事件
public function pay($orderId)
{
$order = Order::findOrFail($orderId);
$order->status = 'paid';
$order->paid_at = date('Y-m-d H:i:s');
$order->save();
Event::trigger(new OrderPaid($order, 'wechat', $order->paid_at));
return json(['code' => 200, 'message' => '支付成功']);
}
public function ship($orderId)
{
$order = Order::findOrFail($orderId);
$trackingNo = input('tracking_no');
$order->status = 'shipped';
$order->tracking_no = $trackingNo;
$order->save();
Event::trigger(new OrderShipped($order, '顺丰速运', $trackingNo));
return json(['code' => 200, 'message' => '发货成功']);
}
此时,每当订单状态变更,所有关联的监听器自动执行,业务代码和通知/统计等副作用完全分离。
七、事件队列化:将监听器转为异步执行
某些监听器(如发送邮件)可能耗时较长,应当放入队列异步执行。ThinkPHP 8 允许将监听器直接配置为队列任务。只需让监听器实现 ShouldQueue 接口:
<?php
namespace applistener;
use thinkqueueShouldQueue;
use appeventOrderPaid;
class SendReceiptListener implements ShouldQueue
{
public function handle(OrderPaid $event): void
{
// 生成并发送电子发票,此方法将在队列中异步执行
generateReceipt($event->order);
sendEmail($event->order->user->email, '您的电子发票');
}
}
同时确保已配置队列驱动(Redis 或 Database)。这样监听器会自动被推送到队列,不会阻塞主请求流程。
八、事件优先级与通配符监听
ThinkPHP 还支持为监听器设置优先级(数字越大越先执行)和通过通配符匹配多个事件:
return [
'bind' => [
appeventOrderCreated::class => [
[applistenerReduceStockListener::class, 10], // 优先级10,先执行
[applistenerSendSmsListener::class, 5], // 优先级5,后执行
],
// 使用通配符监听所有 Order 事件(用于全局日志)
'appeventOrder*' => [
applistenerGlobalAuditListener::class,
],
],
];
优先级确保库存扣减在短信通知前完成,避免超卖后用户仍收到成功通知。通配符则适合审计追踪等全局操作。
九、最佳实践与总结
- 事件命名清晰:使用过去式命名(如
OrderCreated、OrderPaid)表达已发生的动作。 - 事件携带足够上下文:避免监听器自己去数据库查询,将必要的数据直接放在事件对象中,提高性能。
- 一个监听器只做一件事:如果某个监听器开始包含多个
if分支判断不同条件,应拆分为多个独立的监听器。 - 合理使用队列:将耗时操作标记为
ShouldQueue,保持主流程响应迅速。 - 集中配置:尽量在
app/event.php中维护绑定关系,避免遍布各处的注解,方便后期审计事件流。
事件系统是 ThinkPHP 8 中最被低估的能力之一。通过本文的订单生命周期案例,你已经掌握了如何用事件和监听器将复杂业务拆解为松耦合的独立单元。现在,审视你正在开发的项目,找出那些“一个方法做了五件事”的地方,用事件系统重构它,你的代码将变得更加清晰、灵活且易于扩展。

