ThinkPHP 8 模型事件与观察者实战:订单状态流转的自动化钩子

2026-07-30 0 200

在一个电商系统里,订单的状态流转是最核心也最容易出乱子的地方。用户支付成功后订单变“待发货”,管理员发货后变“已发货”,用户确认收货后变“已完成”,超时未支付则变“已取消”。每个状态变更的背后都牵连着一堆动作:记录操作日志、给用户推送通知、更新库存、发放奖励积分。这些动作如果全写在控制器的方法里,每个方法都会变得很长,而且修改其中一个动作时要翻好几个文件。

ThinkPHP 8模型事件让我重新收拾了这一块。模型本身内置了一系列事件——savingsavedupdatingupdateddeletingdeleted等等,可以让我们在模型的生命周期节点上挂载自定义逻辑。再结合观察者类,能把不同类型的状态响应逻辑分门别类地管好。这篇文章就用一个完整的订单状态流转案例,把模型事件和观察者的用法从基础到进阶串一遍。

模型事件的基本机制

每当对一个模型进行增删改查时,框架都会在前后触发一些事件。例如执行$order->save()时,会依次触发saving事件(写入之前)和saved事件(写入之后)。update()会触发updatingupdated。这些事件可以在模型类内部直接定义同名方法来监听,比如:

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调用而显得臃肿,把那些后置操作挪到观察者里,可能是一种立竿见影的重构。

ThinkPHP 8 模型事件与观察者实战:订单状态流转的自动化钩子
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8 模型事件与观察者实战:订单状态流转的自动化钩子 https://www.taomawang.com/server/thinkphp/2455.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务