在一个电商系统里,订单的状态流转是最核心也最容易出乱子的地方。用户支付成功后订单变“待发货”,管理员发货后变“已发货”,用户确认收货后变“已完成”,超时未支付则变“已取消”。每个状态变更的背后都牵连着一堆动作:记录操作日志、给用户推送通知、更新库存、发放奖励积分。这些动作如果全写在控制器的方法里,每个方法都会变得很长,而且修改其中一个动作时要翻好几个文件。
ThinkPHP 8的模型事件让我重新收拾了这一块。模型本身内置了一系列事件——saving、saved、updating、updated、deleting、deleted等等,可以让我们在模型的生命周期节点上挂载自定义逻辑。再结合观察者类,能把不同类型的状态响应逻辑分门别类地管好。这篇文章就用一个完整的订单状态流转案例,把模型事件和观察者的用法从基础到进阶串一遍。
模型事件的基本机制
每当对一个模型进行增删改查时,框架都会在前后触发一些事件。例如执行$order->save()时,会依次触发saving事件(写入之前)和saved事件(写入之后)。update()会触发updating和updated。这些事件可以在模型类内部直接定义同名方法来监听,比如:
namespace appmodel;
use thinkModel;
class Order extends Model
{
public static function onBeforeUpdate(Order $order)
{
// 更新前触发
}
public static function onAfterUpdate(Order $order)
{
// 更新后触发
}
}
这种写法直接、简单,但把所有逻辑都堆在模型类里会让模型逐渐膨胀。更好的做法是用观察者类,将一组相关的模型事件处理逻辑集中在一个独立的类里。
用观察者分离状态处理逻辑
创建一个订单观察者,命令行工具可以直接生成:
php think make:observer OrderObserver
这会在app/observer目录下生成OrderObserver.php,里面已经准备好了几个常用方法。我们重点处理updated事件——当订单更新后,判断状态是否发生了变化,如果变了就执行对应的动作。
namespace appobserver;
use appmodelOrder;
use appserviceLogService;
use appserviceNotificationService;
use appserviceStockService;
class OrderObserver
{
public function onUpdated(Order $order): void
{
// 获取原始状态和变更后的状态
$original = $order->getOriginal('status');
$newStatus = $order->status;
// 如果状态没有变化,什么也不做
if ($original === $newStatus) {
return;
}
// 记录状态变更日志
app(LogService::class)->recordOrderStatusChange(
$order->id, $original, $newStatus
);
// 根据新状态执行分发
match ($newStatus) {
'paid' => $this->handlePaid($order),
'shipped' => $this->handleShipped($order),
'completed' => $this->handleCompleted($order),
'cancelled' => $this->handleCancelled($order),
default => null,
};
}
private function handlePaid(Order $order): void
{
// 支付成功后:发送通知、锁定库存
app(NotificationService::class)->sendOrderPaidNotify($order->user_id, $order->id);
}
private function handleShipped(Order $order): void
{
// 发货后:通知用户物流信息
app(NotificationService::class)->sendOrderShippedNotify($order->user_id, $order->id, $order->tracking_no);
}
private function handleCompleted(Order $order): void
{
// 确认收货后:发放积分、更新商品销量
// 具体逻辑略
}
private function handleCancelled(Order $order): void
{
// 取消订单:回滚库存、退款处理
app(StockService::class)->rollbackStock($order->id);
}
}
观察者写好后,需要把它和模型关联起来。在app/event.php中绑定模型事件和观察者:
return [
'thinkeventModelEvent' => [
// 可以在这里绑定多个模型的观察者
],
];
// 但实际上TP8推荐在模型类里用静态方法注册观察者
// 或者在服务提供者中统一注册
最简单的方式是在模型类的init方法里注册:
namespace appmodel;
use thinkModel;
use appobserverOrderObserver;
class Order extends Model
{
protected static function init()
{
parent::init();
self::observe(OrderObserver::class);
}
}
或者在一个服务提供者里集中注册:
namespace appservice;
use thinkService;
use appmodelOrder;
use appobserverOrderObserver;
class ObserverService extends Service
{
public function boot(): void
{
Order::observe(OrderObserver::class);
}
}
我喜欢用服务提供者的方式,它让所有观察者的注册都在一处管理,不用在每个模型类里重复写。
完整的订单状态流转控制器
现在来看看控制器里怎么用。当管理员需要变更订单状态时,只需更新订单对象并保存,模型事件会自动触发观察者里的逻辑:
namespace appcontroller;
use appmodelOrder;
use thinkRequest;
use thinkexceptionValidateException;
class OrderController
{
public function ship(Request $request)
{
$orderId = $request->post('order_id');
$trackingNo = $request->post('tracking_no');
$order = Order::find($orderId);
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
if ($order->status !== 'paid') {
return json(['code' => 1, 'msg' => '仅待发货状态的订单可发货']);
}
$order->status = 'shipped';
$order->tracking_no = $trackingNo;
$order->save();
// 发货后需要做的事情——发送通知、记录日志——
// 全在OrderObserver里自动执行了,这里不用写
return json(['code' => 0, 'msg' => '发货成功']);
}
public function cancel(Request $request)
{
$orderId = $request->post('order_id');
$order = Order::find($orderId);
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
if (in_array($order->status, ['shipped', 'completed'])) {
return json(['code' => 1, 'msg' => '当前状态不可取消']);
}
$order->status = 'cancelled';
$order->save();
// 库存回滚、退款等操作在观察者中自动处理
return json(['code' => 0, 'msg' => '订单已取消']);
}
}
控制器变得很干净,只负责任务分发和状态校验。至于状态变了之后要做什么,全权交给观察者。业务逻辑之间的边界清晰了很多。
不止有updated:善用其他模型事件
除了updated,其他模型事件也能发挥大作用。比如在订单创建时自动生成唯一订单号:
public function onBeforeInsert(Order $order): void
{
$order->order_no = date('YmdHis') . str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
}
再比如在订单软删除时自动记录删除原因,而不是在控制器里多处调用同一个方法:
public function onBeforeDelete(Order $order): void
{
// 如果不是软删除可以从属性里读取删除原因
app(LogService::class)->recordDelete($order->id, request()->param('delete_reason'));
}
所有对模型的增删改操作都可以通过事件机制挂载横切逻辑,而且事件是按顺序同步执行的,如果某个事件抛出异常,后续事件就不会执行,同时模型的写入也会被回滚(前提是使用事务)。这比在控制器里乱炖各种服务调用要可靠得多。
事件执行顺序和中断
模型事件的执行顺序是固定的:before事件在写入前触发,after事件在写入后触发。在before事件里返回false可以阻止后续操作。比如在更新订单状态前检查库存是否充足,不足则阻止状态变更:
public function onBeforeUpdate(Order $order): bool
{
if ($order->status === 'paid' && $order->getOriginal('status') !== 'paid') {
// 检查库存
if (!app(StockService::class)->checkAndLock($order->id)) {
// 返回false阻止保存
return false;
}
}
return true;
}
需要注意的是,before事件如果有多个监听方法,只要有一个返回false,整个操作就会被阻止。但观察者中的onBeforeUpdate方法如果返回false,只有当前观察者会中断,如果模型类本身还定义了其他onBeforeUpdate静态方法,不会影响它们。实际开发中为了行为可预测,最好统一用观察者处理,不要在模型内部再混写静态方法。
与框架事件系统的区别
之前我们聊过ThinkPHP 8的全局事件系统,用于任意业务事件。模型事件则是专门围绕模型生命周期设计的,更聚焦于数据变更场景。它们的应用层不同:全局事件适合“订单已创建”这种业务事实,可以在任何地方触发;模型事件天然绑定在模型操作上,只要对模型进行了增删改就会自动触发,不需要手动调用Event::trigger。两种机制可以互补使用,比如在观察者的updated方法里再触发一个全局事件,把状态变更的事实广播出去,让其他模块(比如营销系统)也做出响应。
一些容易踩到的细节
获取原始数据。 在updated事件中,模型对象本身已经被修改过了。想要知道修改前的值,需要用$order->getOriginal('字段名')来获取。在updating事件里还没写入,可以直接用$order->status对比getOriginal。
批量更新不会触发事件。 使用Order::where('status', 'pending')->update(['status' => 'expired'])这类批量操作不会触发模型事件,因为框架没有实例化模型对象。如果希望批量操作也触发事件,需要遍历查询结果逐个save,或者使用数据库层面的定时任务配合单个模型操作。
事件中的异常处理。 如果一个观察者方法里抛出了未捕获的异常,整个模型写入会回滚。如果你希望某个通知发送失败不影响订单状态变更(比如邮件服务挂了不应该阻止发货),可以在观察者方法内部try-catch捕获异常并记录日志,而不向上抛出。
总结
模型事件和观察者模式把“数据变更时自动执行某些动作”这件事变成了框架的内建能力,而不是代码里到处乱跑的方法调用。订单状态流转这个例子之所以典型,是因为它天然包含多个状态、每个状态对应多种后续操作——这正是模型事件最擅长的场景。和之前聊过的全局事件系统配合使用,可以让整个应用的响应链变得清晰有序。
如果你现在的项目里控制器还会因为一次save后面跟着七八行service调用而显得臃肿,把那些后置操作挪到观察者里,可能是一种立竿见影的重构。

