PAYATHON 2026

如何在 Shopware 5.4.* 中监听订单变更事件?

支付阿杰

结论

Shopware 5.4.* 没有统一的“订单内容已变更”业务事件,无法同时覆盖订单项、收货地址、账单地址和支付方式的修改。

如果这些修改通过 Shopware 的 Doctrine ORM 完成,建议监听 Doctrine 的 onFlush 和 postFlush 生命周期事件,再检查订单相关实体的变更集。订单状态和支付状态事件只反映对应状态的变化,无法覆盖订单明细和地址实体的增删改。

推荐方案:监听 Doctrine UnitOfWork

Shopware 5.4 的订单数据分布在多个实体中:

  • 订单主体:Shopware\Models\Order\Order
  • 订单项:Shopware\Models\Order\Detail
  • 账单地址:Shopware\Models\Order\Billing
  • 收货地址:Shopware\Models\Order\Shipping

添加、删除或修改订单项,对应的是 Detail 实体的插入、删除或更新。地址变更也是如此。支付方式的修改通常体现为 Order 实体上的支付关联或支付 ID 发生变化。

可以编写一个 Doctrine subscriber:

<?php

namespace MyPlugin\Subscriber;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Shopware\Models\Order\Billing;
use Shopware\Models\Order\Detail;
use Shopware\Models\Order\Order;
use Shopware\Models\Order\Shipping;

class OrderChangeSubscriber implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return [
            Events::onFlush,
        ];
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        $entityManager = $args->getEntityManager();
        $unitOfWork = $entityManager->getUnitOfWork();

        foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
            if ($entity instanceof Detail) {
                $this->handleChange('order_item_added', $entity, []);
            }
        }

        foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
            $changeSet = $unitOfWork->getEntityChangeSet($entity);

            if ($entity instanceof Detail) {
                $this->handleChange('order_item_changed', $entity, $changeSet);
                continue;
            }

            if ($entity instanceof Billing) {
                $this->handleChange('billing_address_changed', $entity, $changeSet);
                continue;
            }

            if ($entity instanceof Shipping) {
                $this->handleChange('shipping_address_changed', $entity, $changeSet);
                continue;
            }

            if ($entity instanceof Order) {
                $this->handleOrderChange($entity, $changeSet);
            }
        }

        foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
            if ($entity instanceof Detail) {
                $this->handleChange('order_item_removed', $entity, []);
            }
        }
    }

    private function handleOrderChange(Order $order, array $changeSet)
    {
        /*
         * 实际字段名应以当前 Shopware 5.4 补丁版本中的
         * Order 实体映射为准。
         */
        $paymentFields = [
            'payment',
            'paymentId',
        ];

        foreach ($paymentFields as $field) {
            if (array_key_exists($field, $changeSet)) {
                $this->handleChange(
                    'payment_method_changed',
                    $order,
                    [$field => $changeSet[$field]]
                );
            }
        }
    }

    private function handleChange($type, $entity, array $changeSet)
    {
        // 在这里记录变更,或先放入待处理队列。
        // 不建议直接在 onFlush 中再次执行 EntityManager::flush()。
    }
}

还需要在插件启动阶段,把订阅器注册到 Shopware 使用的 Doctrine EventManager:

$subscriber = new \MyPlugin\Subscriber\OrderChangeSubscriber();

Shopware()
    ->Models()
    ->getEventManager()
    ->addEventSubscriber($subscriber);

注册代码的具体位置取决于插件采用旧式 Bootstrap 结构,还是 Shopware 5.4 支持的新插件结构。无论采用哪种结构,都要确保每个请求只注册一次,否则同一次变更可能被重复处理。

如何读取变更内容

更新实体时,getEntityChangeSet() 通常会返回以下格式的数据:

[
    'firstname' => ['Old name', 'New name'],
    'city'      => ['Berlin', 'Hamburg'],
]

每个字段都包含旧值和新值,可以据此判断地址中的哪些内容发生了变化。

替换订单项通常不会触发单独的“replace”事件,而会表现为以下两种情况之一:

  • 删除原有 Detail,同时插入新的 Detail
  • 更新原有 Detail 的商品编号、数量、价格等字段

因此,insertion、update 和 deletion 都要处理,不能只监听 update。

后续处理最好放到 postFlush

onFlush 适合读取完整的 Doctrine 变更计划,但不宜直接执行复杂的业务操作,尤其不能在其中再次调用 flush(),否则可能导致递归刷新或 UnitOfWork 状态异常。

可以按以下方式处理:

  1. 在 onFlush 中收集变更。
  2. 在 postFlush 中写入审计日志、投递消息或调用后续服务。
  3. 为外部系统同步失败设置重试机制。

如果还要保存新的 Doctrine 实体,需要谨慎处理 persist()、变更集计算和事务边界。简单场景可以优先把任务投递到独立队列,或者写入不经过 ORM 的日志。

是否可以监听后台控制器事件

也可以订阅类似下面的后台控制器事件:

Enlight_Controller_Action_PostDispatchSecure_Backend_Order

再根据 action 和请求参数判断后台执行的操作。不过,这种方式只能覆盖通过 Shopware 后台订单控制器完成的修改,无法可靠捕获:

  • 自定义插件直接修改订单实体
  • API 写入
  • 定时任务修改
  • 其他服务通过 Doctrine ORM 修改

后台 action 名称和参数属于具体实现细节,升级时也更容易变化。因此,控制器事件适合扩展后台界面,不适合作为完整的订单变更审计入口。

注意事项

  • Doctrine 监听只能捕获当前 EntityManager 管理的实体变更。直接执行 SQL、DBAL 更新或批量 DQL 时,可能不会触发普通的实体生命周期事件。
  • 支付方式在 change set 中对应的字段名要根据实际实体映射确认,不能只依赖示例中的 payment 或 paymentId。
  • 删除订单项时,要在实体仍然可读时保存订单号、商品编号和明细 ID 等所需数据,以免之后访问已经删除的实体或尚未初始化的关联。
  • 如果现有逻辑已经监听订单状态和支付状态事件,需要做好去重,因为 Doctrine subscriber 也可能捕获这些字段的变化。
  • Shopware 5.4 已经是较旧的版本,不同补丁版本和第三方插件可能会改变订单保存流程。上线前应分别测试后台编辑、API 修改、插件修改和订单重新保存等入口。

备注:内容仅供参考。