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,避免促销、运费或税费变化导致提交给网关的金额过期。
备注:内容仅供参考。
