PAYATHON 2026

Sylius 1.12 自定义 Controller 如何获取当前 order 和 cart 信息?

支付小周

明确结论

在 Sylius 1.12 中,购物车就是尚未完成结账的 OrderInterface。自定义 Controller 或 bundle 应注入 Sylius\Component\Order\Context\CartContextInterface,再通过 getCart() 获取当前会话中的 cart/order。不要自行读取 session,也不要直接查询数据库。

订单完成结账后,当前购物车上下文通常不会再指向该订单。这时应使用订单的 tokenValue,通过 sylius.repository.order 查询对应订单。

创建 payment session 时,金额、币种、商品和配送信息都应来自服务端订单。支付卡信息应交给支付网关的托管页面或前端 SDK 收集,不能经过自定义 Controller。

获取当前购物车

<?php

declare(strict_types=1);

namespace App\Controller;

use Sylius\Component\Order\Context\CartContextInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;

final class PaymentSessionController extends AbstractController
{
    private CartContextInterface $cartContext;

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

    public function __invoke(): JsonResponse
    {
        $order = $this->cartContext->getCart();

        return $this->json([
            'order_token' => $order->getTokenValue(),
            'currency' => $order->getCurrencyCode(),
            'items_total' => $order->getItemsTotal(),
            'shipping_total' => $order->getShippingTotal(),
            'tax_total' => $order->getTaxTotal(),
            'total' => $order->getTotal(),
        ]);
    }
}

启用 Symfony autowiring 后,通常可以直接注入该接口。如果项目关闭了自动装配,则需要显式绑定 Sylius 服务:

# config/services.yaml
services:
    App\Controller\PaymentSessionController:
        arguments:
            $cartContext: '@sylius.context.cart'
        tags:
            - 'controller.service_arguments'

CartContextInterface::getCart() 会使用当前请求、session 和 channel 等上下文,所以应在正常的 HTTP 请求中调用。如果无法根据当前请求解析购物车,该方法可能抛出 cart-not-found 类型的异常。此时应返回合适的业务响应,不能继续创建支付会话。

整理 payment session 数据

Sylius 使用最小货币单位保存金额。例如人民币 100.50 元通常保存为整数 10050。不要将金额转换为 float,否则可能出现精度问题。

private function buildPaymentPayload(
    \Sylius\Component\Core\Model\OrderInterface $order
): array {
    $items = [];

    foreach ($order->getItems() as $item) {
        $variant = $item->getVariant();

        $items[] = [
            'name' => $item->getProductName(),
            'variant_code' => $variant !== null ? $variant->getCode() : null,
            'quantity' => $item->getQuantity(),
            'unit_amount' => $item->getUnitPrice(),
            'total_amount' => $item->getTotal(),
        ];
    }

    $shippingAddress = $order->getShippingAddress();
    $customer = $order->getCustomer();

    return [
        'reference' => (string) $order->getTokenValue(),
        'currency' => $order->getCurrencyCode(),
        'amount' => $order->getTotal(),
        'items' => $items,
        'customer' => [
            'email' => $customer !== null ? $customer->getEmail() : null,
        ],
        'shipping' => $shippingAddress === null ? null : [
            'first_name' => $shippingAddress->getFirstName(),
            'last_name' => $shippingAddress->getLastName(),
            'company' => $shippingAddress->getCompany(),
            'street' => $shippingAddress->getStreet(),
            'city' => $shippingAddress->getCity(),
            'postcode' => $shippingAddress->getPostcode(),
            'country_code' => $shippingAddress->getCountryCode(),
            'province_code' => $shippingAddress->getProvinceCode(),
            'province_name' => $shippingAddress->getProvinceName(),
            'phone_number' => $shippingAddress->getPhoneNumber(),
        ],
        'metadata' => [
            'order_number' => $order->getNumber(),
            'order_token' => $order->getTokenValue(),
        ],
    ];
}

具体字段需要按支付网关的 API 调整。有些网关要求商品金额、配送费和税费之和严格等于 amount,另一些网关不接受地址中包含 null 字段。

已完成订单的获取方式

支付返回页、异步 webhook 和结账完成后的页面都不应依赖 CartContextInterface。订单完成后,Sylius 可能已经为用户创建了新的空购物车。

更可靠的做法是,在创建 session 时把订单的 tokenValue 保存为商户侧引用,后续通过订单仓储重新读取:

<?php

use Sylius\Component\Resource\Repository\RepositoryInterface;

final class PaymentCallbackController
{
    private RepositoryInterface $orderRepository;

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

    public function __invoke(string $tokenValue): void
    {
        $order = $this->orderRepository->findOneBy([
            'tokenValue' => $tokenValue,
        ]);

        if ($order === null) {
            throw new \RuntimeException('Order not found.');
        }

        // 校验支付网关签名,并根据订单和支付记录处理结果。
    }
}

对应的服务配置如下:

services:
    App\Controller\PaymentCallbackController:
        arguments:
            $orderRepository: '@sylius.repository.order'

不要只根据前端提交的订单编号查询订单。如果订单标识会出现在公开 URL 中,应优先使用不可预测的 tokenValue,同时检查当前用户权限或验证支付网关签名。

与 Sylius 支付流程的衔接

如果支付网关需要长期维护,建议把创建 payment session 的逻辑放入自定义支付网关或 Payum action,不要全部写在 Controller 中。Controller 负责启动流程,支付组件则负责:

  • 从订单关联的 PaymentInterface 读取金额和币种;
  • 调用网关创建 session;
  • 保存网关 session ID、transaction ID 等引用;
  • 跳转到托管支付页面;
  • 处理回调、状态查询和 Sylius payment state 更新。

这样仍可使用 Sylius 的 payment state machine,避免网关已经扣款,而 Sylius 订单仍显示未支付。

如果暂时由 Controller 直接调用网关,至少要先完成以下检查:

if ($order->isEmpty()) {
    throw new \DomainException('Cannot pay for an empty cart.');
}

if ($order->getTotal() <= 0) {
    throw new \DomainException('Order amount must be greater than zero.');
}

if ($order->getCurrencyCode() === null) {
    throw new \DomainException('Order currency is missing.');
}

if ($order->getShippingAddress() === null) {
    throw new \DomainException('Shipping address is missing.');
}

是否必须提供配送地址,要看订单中是否包含实体商品。纯数字商品不一定需要配送信息。

安全与一致性注意事项

  • 金额、币种和商品价格必须从 Sylius 服务端订单读取,不能信任浏览器提交的金额。
  • 创建支付会话前,应确认订单仍可支付,并检查关联 payment 是否已经成功,防止重复付款。
  • 使用订单 token 或 payment ID 作为 idempotency key,避免用户刷新页面后重复创建 session。
  • API 密钥应保存在环境变量或 secrets 中,不能返回给浏览器,也不能写入日志。
  • 只通过 HTTPS 与支付网关通信,并验证 webhook 签名、时间戳、交易金额和币种。
  • 服务器不能接收、保存或记录完整卡号、CVV 等卡片数据。应优先使用网关托管页面或 tokenization SDK。
  • webhook 中的 session ID 只能用于定位记录,不能单独证明支付成功。还需要验证签名,并向网关查询最终状态。
  • 订单完成后,应通过订单仓储重新加载数据,不能继续假定当前 cart 就是原订单。
  • 创建 session 前,最好刷新或校验订单 totals,避免促销、运费或税费变化导致提交给网关的金额过期。

备注:内容仅供参考。