Sylius 移除配送和支付步骤后重定向到 Address
结论
出现 302 重定向,是因为 checkout state machine 的配置不完整:payment_selected 状态已经删除,但 complete transition 仍要求从 payment_selected 开始。提交地址后,订单实际处于 addressed 状态,无法执行 complete。checkout resolver 随后根据当前状态,将请求重新导向 Address 页面。
先把 complete transition 改为从 addressed 进入 completed:
winzou_state_machine:
sylius_order_checkout:
states:
cart: ~
addressed: ~
completed: ~
transitions:
address:
from: [cart, addressed]
to: addressed
complete:
from: [addressed]
to: completed
还要注意,隐藏 shipping 和 payment 页面,并不会自动为订单选择配送方式和支付方式。Address 提交成功后,必须为订单中的 Shipment 和 Payment 设置 method,否则最终校验、费用计算或支付跳转仍可能失败。
为什么会重定向回 Address
当前配置中有一个永远无法满足的 transition:
complete:
from: [payment_selected]
to: completed
但定义的状态只有:
cart
addressed
completed
实际执行过程是:
cart
└─ address → addressed
└─ complete → 要求 payment_selected,无法执行
debug:config winzou_state_machine 只显示 Symfony 最终加载的配置,不会检查 transition 的来源状态是否存在。因此,命令输出中仍有 payment_selected,不代表配置有效。相反,这正是问题所在。
正确流程应当是:
cart
└─ address → addressed
└─ complete → completed
修改后再次运行:
php bin/console debug:config winzou_state_machine
确认最终结果类似:
states:
cart: null
addressed: null
completed: null
transitions:
address:
from:
- cart
- addressed
to: addressed
complete:
from:
- addressed
to: completed
然后清理对应环境的缓存:
php bin/console cache:clear
自动指定配送方式和支付方式
删除选择页面后,需要把原本由 select_shipping 和 select_payment 表单完成的工作移到 Address 提交之后。
执行顺序应当是:
- 将地址写入订单;
- 等待 Sylius 根据订单内容创建或更新
Shipment和Payment; - 从当前渠道可用的方法中选择默认项;
- 调用
setMethod(); - 重新处理订单金额并持久化;
- 进入 Complete 页面。
不能只在模板中隐藏字段,因为 method 必须保存到订单关联的 Shipment 和 Payment 对象中。
下面是一个领域服务示例。不同 Sylius 版本使用的 resolver 接口、服务 ID 和订单处理器名称可能不同,具体命名应以项目当前版本为准,但核心逻辑相同。
<?php
namespace App\Checkout;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Sylius\Component\Payment\Resolver\PaymentMethodsResolverInterface;
use Sylius\Component\Shipping\Resolver\ShippingMethodsResolverInterface;
final class DefaultCheckoutMethodsAssigner
{
private ShippingMethodsResolverInterface $shippingMethodsResolver;
private PaymentMethodsResolverInterface $paymentMethodsResolver;
private OrderProcessorInterface $orderProcessor;
public function __construct(
ShippingMethodsResolverInterface $shippingMethodsResolver,
PaymentMethodsResolverInterface $paymentMethodsResolver,
OrderProcessorInterface $orderProcessor
) {
$this->shippingMethodsResolver = $shippingMethodsResolver;
$this->paymentMethodsResolver = $paymentMethodsResolver;
$this->orderProcessor = $orderProcessor;
}
public function assign(OrderInterface $order): void
{
// 确保 Shipment、Payment 以及订单金额已经生成或更新。
$this->orderProcessor->process($order);
foreach ($order->getShipments() as $shipment) {
if (null !== $shipment->getMethod()) {
continue;
}
$methods = $this->shippingMethodsResolver->getSupportedMethods($shipment);
if (1 !== count($methods)) {
throw new \DomainException(sprintf(
'Expected exactly one shipping method, got %d.',
count($methods)
));
}
$shipment->setMethod(reset($methods));
}
foreach ($order->getPayments() as $payment) {
if (null !== $payment->getMethod()) {
continue;
}
$methods = $this->paymentMethodsResolver->getSupportedMethods($payment);
if (1 !== count($methods)) {
throw new \DomainException(sprintf(
'Expected exactly one payment method, got %d.',
count($methods)
));
}
$payment->setMethod(reset($methods));
}
// 配送费用、税费和支付金额可能依赖所选 method。
$this->orderProcessor->process($order);
}
}
这里要求 resolver 恰好返回一个 method,而不是直接取数组中的第一项。这样,如果后台误启用了第二种方式,或者当前地址没有可用的配送方式,系统会抛出明确错误,而不会悄悄选中一个不确定的结果。
如果业务允许使用多个 method,但其中有一个默认项,应根据稳定标识选择,例如 method code:
private function findMethodByCode(iterable $methods, string $expectedCode): object
{
foreach ($methods as $method) {
if ($method->getCode() === $expectedCode) {
return $method;
}
}
throw new \DomainException(sprintf(
'Method "%s" is not available for this order.',
$expectedCode
));
}
不要依赖数据库的返回顺序,也不要按名称选择,因为名称通常可以翻译和修改。
在 Address 提交后调用服务
路由中的:
_sylius:
event: address
会让资源控制器在更新过程中派发与 address 相关的事件。具体事件名称取决于项目使用的 Sylius/ResourceBundle 版本。常见形式是 sylius.order.post_address,但必须通过当前版本的事件定义或调试命令确认,不能直接猜测。
确认事件名称后,可以注册 subscriber:
<?php
namespace App\EventSubscriber;
use App\Checkout\DefaultCheckoutMethodsAssigner;
use Sylius\Component\Core\Model\OrderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Sylius\Component\Resource\Event\ResourceControllerEvent;
final class AssignCheckoutMethodsSubscriber implements EventSubscriberInterface
{
public function __construct(
private DefaultCheckoutMethodsAssigner $assigner
) {
}
public static function getSubscribedEvents(): array
{
return [
'sylius.order.post_address' => 'onPostAddress',
];
}
public function onPostAddress(ResourceControllerEvent $event): void
{
$order = $event->getSubject();
if (!$order instanceof OrderInterface) {
return;
}
$this->assigner->assign($order);
}
}
服务配置示例:
services:
App\Checkout\DefaultCheckoutMethodsAssigner:
arguments:
$shippingMethodsResolver: '@sylius.shipping_methods_resolver'
$paymentMethodsResolver: '@sylius.payment_methods_resolver'
$orderProcessor: '@sylius.order_processing.order_processor'
App\EventSubscriber\AssignCheckoutMethodsSubscriber:
arguments:
$assigner: '@App\Checkout\DefaultCheckoutMethodsAssigner'
tags:
- { name: kernel.event_subscriber }
这三个服务 ID 在不同 Sylius 版本中可能不一致。如果容器找不到服务,先查询项目实际注册的服务:
php bin/console debug:container | grep -E 'shipping.*resolver|payment.*resolver|order.*processor'
如果当前版本的 post_address 事件发生在 Doctrine flush 之后,还需要在 listener 中显式执行持久化和 flush。也可以改用自定义 Address action,在同一个事务中完成地址更新、method 分配和订单处理。无论采用哪种方式,进入 Complete 页面前都必须保存选择结果。
路由和 resolver 配置
checkout resolver 可以只保留两个有效状态:
sylius_shop:
checkout_resolver:
route_map:
cart:
route: sylius_shop_checkout_address
addressed:
route: sylius_shop_checkout_complete
通常不需要为 completed 添加 checkout 路由,因为订单完成后会进入感谢页或支付流程。
Address 路由跳转到 Complete 的配置没有问题:
redirect:
route: sylius_shop_checkout_complete
parameters: []
Complete 路由中的支付跳转要求订单已经有一个新的 Payment:
paymentId: expr:service('sylius.context.cart').getCart().getLastNewPayment().getId()
如果使用线下支付、免费支付,或者支付流程不经过 sylius_shop_order_pay,这里还要改为对应的订单完成页。否则即使 checkout 已成功完成,后续仍可能因为 getLastNewPayment() 返回 null 或支付路由不适用而报错。
还需要检查的条件
自动选择 method 时,还要确认:
- 配送方式已经启用,并属于订单当前 channel。
- 配送方式的区域、地址和规则允许当前
Shipment使用。 - 支付方式已经启用,并属于订单当前 channel。
- 提交地址后,已经生成至少一个
Shipment。 - 处理订单后,已经生成至少一个
Payment。 - Complete 表单的
sylius_checkout_completevalidation group 不再要求用户经过已经删除的中间状态。 - 自定义 state machine 配置覆盖的是正确 graph,没有意外追加到另一份配置。
需要完成的两项主要修改是:把 complete.from 改为 addressed,并在 Address transition 完成后,对现有的 Shipment 和 Payment 调用 setMethod()。只修改页面和路由,无法代替这两部分业务状态处理。
备注:内容仅供参考。
