ThinkPHP8 模型事件烂尾之后,我把业务逻辑搬到了事件监听器

2026-08-02 0 109

上月接手一个电商项目,订单模块里有一个“订单状态变更”的操作。原开发在 Order 模型里写了不少模型事件,用来在状态改变后发短信、记录日志、更新会员等级。刚上线那会儿还好,可随着业务增加,模型事件里的代码越来越乱,各种 if 套 if,看着就头大。后来我下决心用事件监听器重构了这块,代码清爽了不止一倍。

这里不踩模型事件,只是提供一个更适合复杂业务的玩法。下面用一个小案例带你走一遍:订单支付成功后,需要通知用户、写一条操作日志、统计订单金额。你会看到模型事件怎么写,以及为什么我最后换成了事件监听器。

一、为什么一开始会选模型事件

ThinkPHP 8 的模型支持事件钩子,比如 afterInsertafterUpdate 等。在模型类内部定义这些方法,就能在写入后自动触发。以订单为例,原来代码大概长这样:

<?php
namespace appmodel;

use thinkModel;
use thinkfacadeLog;

class Order extends Model
{
    // 订单状态变更后触发的模型事件
    public static function onAfterUpdate(Order $order)
    {
        // 只处理状态为已支付的情况
        if ($order->status == 1) {
            // 1. 发短信通知用户
            SmsService::send($order->phone, '您的订单已支付成功');
            // 2. 记录日志
            Log::info('订单已支付: ' . $order->order_no);
            // 3. 更新会员等级积分
            User::where('id', $order->user_id)->inc('points', $order->amount)->update();
        }
    }
}

刚开始这么写很方便,因为不需要额外注册什么。可后来订单状态变更的地方越来越多,有管理后台手动改、用户取消、超时关闭,还有回调通知。每个入口都有可能更新订单,也就都会触发 onAfterUpdate。问题来了:

  • 模型事件里混杂了“发送短信”“写日志”“积分更新”三类无关逻辑,违反了单一职责。
  • 有时候第三方通知回调里更新了订单,我不想让它发短信(比如同步数据时会误触发)。模型事件无法很方便地屏蔽。
  • 如果将来要增加“发送邮件”或者“推送小程序订阅消息”,还得改模型类,扩展性差。

二、事件监听器,把业务拆成独立模块

ThinkPHP 的事件系统本来就是为了解决这类问题。和模型事件不同,事件监听器可以让“谁关心这个事件”自己去监,而不是在模型里写死。核心思路是:订单模型只负责数据更新,更新完成后抛出一个“订单已支付”的事件,然后由监听器去处理不同的任务。

这样做的好处是:模型不需要知道自己更新后要做什么,每个监听器只做一件事,互不干扰。而且我可以按需监听,不想触发时直接不监听,或者用事件类自带的“传播”特性来控制。

三、一步步重构订单支付事件

3.1 定义事件类

app/event.php 中注册事件定义,或者用更灵活的方式:直接创建一个事件类,携带订单数据。这里我选择建一个 OrderPaid 事件类,专门代表“订单支付成功”这个动作。

<?php
namespace appevent;

class OrderPaid
{
    public $order;

    public function __construct($order)
    {
        $this->order = $order;
    }
}

3.2 创建监听器

分别创建三个监听器类,各司其职。比如 NotifyUser 负责发送通知,WriteOrderLog 负责记录日志,UpdateUserLevel 负责更新会员等级。

<?php
namespace applistener;

class NotifyUser
{
    public function handle(OrderPaid $event)
    {
        // 这里使用你项目的短信服务
        SmsService::send($event->order->phone, '您的订单已支付成功');
    }
}
<?php
namespace applistener;

use thinkfacadeLog;

class WriteOrderLog
{
    public function handle(OrderPaid $event)
    {
        Log::info('订单已支付: ' . $event->order->order_no);
    }
}
<?php
namespace applistener;

class UpdateUserLevel
{
    public function handle(OrderPaid $event)
    {
        User::where('id', $event->order->user_id)
            ->inc('points', $event->order->amount)
            ->update();
    }
}

3.3 注册监听器

app/event.php 文件中,将事件与监听器关联起来。

<?php
return [
    'bind' => [
        'OrderPaid' => 'appeventOrderPaid',
    ],
    'listen' => [
        'appeventOrderPaid' => [
            'applistenerNotifyUser',
            'applistenerWriteOrderLog',
            'applistenerUpdateUserLevel',
        ],
    ],
];

如果你用的是模块化架构,也可以把配置文件放到模块目录下。这里全局注册最省事。

3.4 在订单模型更新后触发事件

接下来,在 Order 模型里不再直接写业务逻辑,而是调用事件触发。我选择用模型的 afterUpdate 钩子来触发,因为支付成功就是一个更新动作。

<?php
namespace appmodel;

use thinkModel;
use thinkfacadeEvent;

class Order extends Model
{
    public static function onAfterUpdate(Order $order)
    {
        // 只有状态变为已支付时才触发业务事件
        if ($order->status == 1) {
            Event::trigger(new appeventOrderPaid($order));
        }
    }
}

同样的,控制器里仍然只需要更新订单状态。但后续的所有额外操作都自动跑在监听器里了。

3.5 在控制器里如何调用

<?php
namespace appcontroller;

use appmodelOrder;
use thinkRequest;

class OrderController
{
    public function pay(Request $request)
    {
        $order = Order::find($request->param('order_id'));
        // 模拟支付成功置状态为1
        $order->status = 1;
        $order->save();
        return json(['code' => 0, 'msg' => '支付成功']);
    }
}

你不需要在控制器里手动触发事件,模型钩子会自动完成。当然,如果你希望某些场景不触发,可以在模型里设置一个临时属性来跳过。例如:

// 临时忽略事件
$order->withEvent(false)->save(); // ThinkPHP8 已内置这个方法

这比在模型事件里塞各种判断简单多了。

四、比模型事件强在哪

重构后最大的感受是,每个文件篇幅小了很多,排查问题只看对应监听器就行。而且当我需要新增一个“发送企业微信通知”的时候,只需要新建一个监听器,然后在 event.php 里加一行,不动任何现有代码。

还有一点:监听器支持按需动态监听。比如临时只想停掉短信通知,直接在 event.php 里把 NotifyUser 注释掉就好,不用跑到模型里删代码。这要是放在以前,得去翻那份复杂的 if 代码。

五、绕不过的坑

5.1 事件监听器是同步执行的

ThinkPHP 的事件默认是同步运行。也就是说,监听器里的短信、日志、积分更新全部阻塞在订单 save() 之后的流程里。如果某个监听器执行慢,会影响接口响应。对于发短信这种外部调用,建议把监听器改成异步任务,比如放到队列里。

我的做法是:在监听器的 handle 方法里把真正耗时的操作投递到队列。这里不展开队列的具体写法,但你可以用 ThinkPHP 自带的 Queue。

5.2 事件类对象传递的坑

如果你的监听器要修改订单的数据,并且希望后续其他监听器能看到修改,那就需要传引用。比如在 NotifyUser 里改了 $event->order->remark,而 WriteOrderLog 依赖这个字段。事件类构造函数里最好写 &$order 来强制引用。

否则你拿到的就是一个副本,改了不影响后续监听器。我一开始没注意,导致日志里打印的备注一直是旧的,折腾了半天。

5.3 不要和模型事件混用相同逻辑

改完以后,最好把原来模型事件里的业务逻辑全部删掉,只保留触发事件这一句。不然会出现“发两次短信”的情况。我们在代码评审时重点关注了这个点。

六、进阶玩法:事件订阅

当监听器过多时,可以用事件订阅器把相关监听器捆在一起。一个订阅器类里可以监听多个事件,让代码归拢。

<?php
namespace applistener;

class OrderSubscriber
{
    public function onOrderPaid(OrderPaid $event)
    {
        // ...
    }

    public function onOrderCancel(OrderCanceled $event)
    {
        // ...
    }

    public function subscribe($events)
    {
        $events->listen('OrderPaid', [$this, 'onOrderPaid']);
        $events->listen('OrderCanceled', [$this, 'onOrderCancel']);
    }
}

然后注册这个订阅器到 event.php 中。这种方式适合一个领域下的事件比较多时,便于维护。

七、最后

经过这次重构,我深刻体会到:模型事件适合做“模型内部自身状态的一致性维护”,而跨领域、多副作用的业务动作,用事件监听器才是正解。它让模型保持干净,让业务逻辑自治还灵活。

如果你的项目里也有类似的一堆模型钩子和越来越重的模型事件,强烈建议试试事件监听器。它不会让你多写太多代码,但后期维护时你会谢现在的自己。

以上代码都是基于 ThinkPHP 8.0 版本编写,目录结构和命名空间请根据你的项目微调。欢迎交流各自踩过的坑。

ThinkPHP8 模型事件烂尾之后,我把业务逻辑搬到了事件监听器
收藏 (0) 打赏

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

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

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

淘吗网 thinkphp ThinkPHP8 模型事件烂尾之后,我把业务逻辑搬到了事件监听器 https://www.taomawang.com/server/thinkphp/2476.html

常见问题

相关文章

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

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