This commit is contained in:
SyliusBot 2026-06-26 03:08:36 +00:00 committed by GitHub
commit 378719b70b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 325 additions and 30 deletions

View file

@ -44,6 +44,33 @@
(`identity_generation_preferences` for `PostgreSQLPlatform`) to keep the database schema backward compatible
with existing installations.
3. The `_sylius: redirect:` block has been removed from the `sylius_shop_order_pay` route definition.
Previously, `PayumPayResponseProvider` read the after-pay redirect route at runtime from the routing metadata:
```yaml
# ShopBundle/Resources/config/routing/order.yml (removed)
sylius_shop_order_pay:
defaults:
_sylius:
redirect:
route: sylius_shop_order_after_pay
```
The redirect route is now resolved from the `sylius_shop` bundle configuration at compile time.
If you customized the after-pay redirect route for Payum, use the dedicated Payum configuration keys instead:
```yaml
sylius_shop:
order_pay:
payum_after_pay_route: sylius_shop_order_after_pay
payum_after_pay_route_parameters: []
```
> **Note:** The `after_pay_route` and `after_pay_route_parameters` keys are reserved for the Payment Request flow
> and their default parameters include Payment Request-specific values (e.g. `hash: paymentRequest.getHash()`).
> Do not use them to configure the Payum after-pay redirect.
## Dependencies
1. The `behat/transliterator` package has been **deprecated** and will be removed in Sylius 3.0.

View file

@ -39,7 +39,7 @@ final class ProductVariantNormalizer implements NormalizerInterface, NormalizerA
private const ALREADY_CALLED = 'sylius_product_variant_normalizer_already_called';
public function __construct(
private readonly ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $priceCalculator,
private readonly CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $priceCalculator,
private readonly AvailabilityCheckerInterface $availabilityChecker,
private readonly SectionProviderInterface $uriBasedSectionContext,
private readonly IriConverterInterface $iriConverter,

View file

@ -34,7 +34,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ProductVariantNormalizerTest extends TestCase
{
private MockObject&CatalogPricesCalculatorInterface $pricesCalculator;
private CatalogPricesCalculatorInterface&MockObject $pricesCalculator;
private AvailabilityCheckerInterface&MockObject $availabilityChecker;

View file

@ -23,7 +23,7 @@ use Webmozart\Assert\Assert;
final class PriceExtension extends AbstractExtension
{
public function __construct(
private readonly ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $productVariantPricesCalculator,
private readonly CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $productVariantPricesCalculator,
) {
if (!$this->productVariantPricesCalculator instanceof CatalogPricesCalculatorInterface) {
trigger_deprecation(

View file

@ -29,9 +29,14 @@ use Webmozart\Assert\Assert;
/** @experimental */
final class PayumPayResponseProvider implements PayResponseProviderInterface
{
/**
* @param array<string, string> $afterPayUrlParameters
*/
public function __construct(
private Payum $payum,
private PaymentToPayResolverInterface $paymentToPayResolver,
private readonly ?string $afterPayUrlRoute = null,
private readonly array $afterPayUrlParameters = [],
) {
}
@ -42,13 +47,7 @@ final class PayumPayResponseProvider implements PayResponseProviderInterface
$payment = $this->paymentToPayResolver->getPayment($order);
Assert::notNull($payment, sprintf('Order (id %s) must have last payment in state "new".', $order->getId()));
$redirectOptions = $requestConfiguration->getParameters()->get('redirect');
if (is_string($redirectOptions)) {
$redirectOptions = [
'route' => $redirectOptions,
];
}
$token = $this->provideTokenBasedOnPayment($payment, $redirectOptions);
$token = $this->provideTokenBasedOnPayment($payment);
return new RedirectResponse($token->getTargetUrl());
}
@ -74,10 +73,7 @@ final class PayumPayResponseProvider implements PayResponseProviderInterface
return $gatewayConfig->getUsePayum();
}
/**
* @param array{route: ?string, parameters: ?string[]} $redirectOptions
*/
private function provideTokenBasedOnPayment(PaymentInterface $payment, array $redirectOptions): TokenInterface
private function provideTokenBasedOnPayment(PaymentInterface $payment): TokenInterface
{
$gatewayConfig = $this->getGatewayConfigFromPayment($payment);
Assert::notNull($gatewayConfig, 'An existing gateway config must exist.');
@ -89,16 +85,16 @@ final class PayumPayResponseProvider implements PayResponseProviderInterface
return $tokenFactory->createAuthorizeToken(
$gatewayConfig->getGatewayName(),
$payment,
$redirectOptions['route'] ?? null,
$redirectOptions['parameters'] ?? [],
$this->afterPayUrlRoute,
$this->afterPayUrlParameters,
);
}
return $tokenFactory->createCaptureToken(
$gatewayConfig->getGatewayName(),
$payment,
$redirectOptions['route'] ?? null,
$redirectOptions['parameters'] ?? [],
$this->afterPayUrlRoute,
$this->afterPayUrlParameters,
);
}

View file

@ -19,7 +19,11 @@ return static function (ContainerConfigurator $container) {
$services
->set('sylius_shop.provider.order_pay.pay_response.payum')
->parent('sylius_payum.provider.order_pay.pay_response')
->args([service('sylius_shop.resolver.order_pay.payment_to_pay')])
->args([
service('sylius_shop.resolver.order_pay.payment_to_pay'),
'%sylius_shop.order_pay.payum_after_pay_route%',
'%sylius_shop.order_pay.payum_after_pay_route_parameters%',
])
->tag('sylius_shop.provider.order_pay.pay_response', ['priority' => -200])
;

View file

@ -0,0 +1,186 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Tests\Sylius\Bundle\PayumBundle\OrderPay\Provider;
use Payum\Core\Payum;
use Payum\Core\Security\GenericTokenFactoryInterface;
use Payum\Core\Security\TokenInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\CoreBundle\OrderPay\Resolver\PaymentToPayResolverInterface;
use Sylius\Bundle\PayumBundle\Model\GatewayConfig;
use Sylius\Bundle\PayumBundle\OrderPay\Provider\PayumPayResponseProvider;
use Sylius\Bundle\ResourceBundle\Controller\RequestConfiguration;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
final class PayumPayResponseProviderTest extends TestCase
{
private MockObject&Payum $payum;
private MockObject&PaymentToPayResolverInterface $paymentToPayResolver;
private GenericTokenFactoryInterface&MockObject $tokenFactory;
private PayumPayResponseProvider $provider;
protected function setUp(): void
{
$this->payum = $this->createMock(Payum::class);
$this->paymentToPayResolver = $this->createMock(PaymentToPayResolverInterface::class);
$this->tokenFactory = $this->createMock(GenericTokenFactoryInterface::class);
$this->provider = new PayumPayResponseProvider(
$this->payum,
$this->paymentToPayResolver,
'app_shop_order_after_pay',
['tokenValue' => 'fixed-token-value'],
);
}
public function testItCreatesCaptureTokenWithConfiguredAfterPayRouteAndParameters(): void
{
$requestConfiguration = $this->createStub(RequestConfiguration::class);
$order = $this->createOrder();
$payment = $this->createPaymentWithGatewayConfig(['use_authorize' => false]);
$token = $this->createMock(TokenInterface::class);
$this->paymentToPayResolver
->expects(self::once())
->method('getPayment')
->with($order)
->willReturn($payment)
;
$this->payum->expects(self::once())->method('getTokenFactory')->willReturn($this->tokenFactory);
$this->tokenFactory
->expects(self::once())
->method('createCaptureToken')
->with(
'offline',
$payment,
'app_shop_order_after_pay',
['tokenValue' => 'fixed-token-value'],
)
->willReturn($token)
;
$this->tokenFactory->expects(self::never())->method('createAuthorizeToken');
$token->expects(self::once())->method('getTargetUrl')->willReturn('/payum/capture');
$response = $this->provider->getResponse($requestConfiguration, $order);
self::assertInstanceOf(RedirectResponse::class, $response);
self::assertSame('/payum/capture', $response->getTargetUrl());
}
public function testItCreatesAuthorizeTokenWithConfiguredAfterPayRouteAndParameters(): void
{
$requestConfiguration = $this->createStub(RequestConfiguration::class);
$order = $this->createOrder();
$payment = $this->createPaymentWithGatewayConfig(['use_authorize' => true]);
$token = $this->createMock(TokenInterface::class);
$this->paymentToPayResolver
->expects(self::once())
->method('getPayment')
->with($order)
->willReturn($payment)
;
$this->payum->expects(self::once())->method('getTokenFactory')->willReturn($this->tokenFactory);
$this->tokenFactory
->expects(self::once())
->method('createAuthorizeToken')
->with(
'offline',
$payment,
'app_shop_order_after_pay',
['tokenValue' => 'fixed-token-value'],
)
->willReturn($token)
;
$this->tokenFactory->expects(self::never())->method('createCaptureToken');
$token->expects(self::once())->method('getTargetUrl')->willReturn('/payum/authorize');
$response = $this->provider->getResponse($requestConfiguration, $order);
self::assertInstanceOf(RedirectResponse::class, $response);
self::assertSame('/payum/authorize', $response->getTargetUrl());
}
public function testItSupportsOrdersWithPayumEnabledPayment(): void
{
$requestConfiguration = $this->createStub(RequestConfiguration::class);
$order = $this->createOrder();
$payment = $this->createPaymentWithGatewayConfig([]);
$this->paymentToPayResolver
->expects(self::once())
->method('getPayment')
->with($order)
->willReturn($payment)
;
self::assertTrue($this->provider->supports($requestConfiguration, $order));
}
public function testItDoesNotSupportOrdersWithPayumDisabledPayment(): void
{
$requestConfiguration = $this->createStub(RequestConfiguration::class);
$order = $this->createOrder();
$gatewayConfig = new GatewayConfig();
$gatewayConfig->setUsePayum(false);
$payment = $this->createPayment($gatewayConfig);
$this->paymentToPayResolver
->expects(self::once())
->method('getPayment')
->with($order)
->willReturn($payment)
;
self::assertFalse($this->provider->supports($requestConfiguration, $order));
}
private function createOrder(): MockObject&OrderInterface
{
$order = $this->createMock(OrderInterface::class);
$order->method('getId')->willReturn(1);
return $order;
}
/**
* @param array<string, mixed> $config
*/
private function createPaymentWithGatewayConfig(array $config): MockObject&PaymentInterface
{
$gatewayConfig = new GatewayConfig();
$gatewayConfig->setGatewayName('offline');
$gatewayConfig->setConfig($config);
return $this->createPayment($gatewayConfig);
}
private function createPayment(GatewayConfig $gatewayConfig): MockObject&PaymentInterface
{
$paymentMethod = $this->createMock(PaymentMethodInterface::class);
$paymentMethod->method('getGatewayConfig')->willReturn($gatewayConfig);
$payment = $this->createMock(PaymentInterface::class);
$payment->method('getMethod')->willReturn($paymentMethod);
return $payment;
}
}

View file

@ -91,6 +91,14 @@ final class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->scalarNode('payum_after_pay_route')
->defaultValue('sylius_shop_order_after_pay')
->end()
->arrayNode('payum_after_pay_route_parameters')
->defaultValue([])
->useAttributeAsKey('name')
->scalarPrototype()->end()
->end()
->scalarNode('final_route')
->defaultValue('sylius_shop_order_thank_you')
->end()

View file

@ -134,6 +134,8 @@ final class SyliusShopExtension extends Extension implements PrependExtensionInt
$container->setParameter('sylius_shop.order_pay.payment_request_pay_route_parameters', $config['payment_request_pay_route_parameters']);
$container->setParameter('sylius_shop.order_pay.after_pay_route', $config['after_pay_route']);
$container->setParameter('sylius_shop.order_pay.after_pay_route_parameters', $config['after_pay_route_parameters']);
$container->setParameter('sylius_shop.order_pay.payum_after_pay_route', $config['payum_after_pay_route']);
$container->setParameter('sylius_shop.order_pay.payum_after_pay_route_parameters', $config['payum_after_pay_route_parameters']);
$container->setParameter('sylius_shop.order_pay.final_route', $config['final_route']);
$container->setParameter('sylius_shop.order_pay.final_route_parameters', $config['final_route_parameters']);
$container->setParameter('sylius_shop.order_pay.retry_route', $config['retry_route']);

View file

@ -14,9 +14,6 @@ sylius_shop_order_pay:
methods: [GET]
defaults:
_controller: sylius_shop.controller.order_pay::payAction
_sylius:
redirect:
route: sylius_shop_order_after_pay
sylius_shop_order_after_pay:
path: /after-pay/{hash?}

View file

@ -49,7 +49,7 @@ class CardComponent
protected readonly ProductVariantResolverInterface $productVariantResolver,
protected readonly ChannelContextInterface $channelContext,
protected readonly LocaleContextInterface $localeContext,
protected readonly ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $productVariantPricesCalculator,
protected readonly CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $productVariantPricesCalculator,
) {
if (!$this->productVariantPricesCalculator instanceof CatalogPricesCalculatorInterface) {
trigger_deprecation(

View file

@ -44,7 +44,7 @@ class PriceComponent
public bool $hasDiscount = false;
public function __construct(
protected readonly ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $productVariantPricesCalculator,
protected readonly CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $productVariantPricesCalculator,
protected readonly MoneyFormatterInterface $moneyFormatter,
protected readonly ChannelContextInterface $channelContext,
protected readonly LocaleContextInterface $localeContext,

View file

@ -149,6 +149,55 @@ final class ConfigurationTest extends TestCase
]]);
}
#[Test]
public function it_has_default_configuration_for_order_pay_node(): void
{
$this->assertProcessedConfigurationEquals(
[[]],
['order_pay' => [
'payment_request_pay_route' => 'sylius_shop_payment_request_pay',
'payment_request_pay_route_parameters' => ['hash' => 'paymentRequest.getHash()'],
'after_pay_route' => 'sylius_shop_order_after_pay',
'after_pay_route_parameters' => ['hash' => 'paymentRequest.getHash()'],
'payum_after_pay_route' => 'sylius_shop_order_after_pay',
'payum_after_pay_route_parameters' => [],
'final_route' => 'sylius_shop_order_thank_you',
'final_route_parameters' => [],
'retry_route' => 'sylius_shop_order_show',
'retry_route_parameters' => ['tokenValue' => 'order.getTokenValue()'],
]],
'order_pay',
);
}
#[Test]
public function its_payum_after_pay_redirect_is_configurable(): void
{
$this->assertProcessedConfigurationEquals(
[[
'order_pay' => [
'payum_after_pay_route' => 'app_shop_order_after_pay',
'payum_after_pay_route_parameters' => ['tokenValue' => 'fixed-token-value'],
],
]],
[
'order_pay' => [
'payum_after_pay_route' => 'app_shop_order_after_pay',
'payum_after_pay_route_parameters' => ['tokenValue' => 'fixed-token-value'],
'payment_request_pay_route' => 'sylius_shop_payment_request_pay',
'payment_request_pay_route_parameters' => ['hash' => 'paymentRequest.getHash()'],
'after_pay_route' => 'sylius_shop_order_after_pay',
'after_pay_route_parameters' => ['hash' => 'paymentRequest.getHash()'],
'final_route' => 'sylius_shop_order_thank_you',
'final_route_parameters' => [],
'retry_route' => 'sylius_shop_order_show',
'retry_route_parameters' => ['tokenValue' => 'order.getTokenValue()'],
],
],
'order_pay',
);
}
protected function getConfiguration(): Configuration
{
return new Configuration();

View file

@ -116,6 +116,32 @@ final class SyliusShopExtensionTest extends AbstractExtensionTestCase
$this->assertContainerBuilderHasParameter('sylius_shop.firewall_context_name', 'myshopfirewall');
}
#[Test]
public function it_configures_default_order_pay_parameters(): void
{
$this->load([]);
$this->assertContainerBuilderHasParameter('sylius_shop.order_pay.payum_after_pay_route', 'sylius_shop_order_after_pay');
$this->assertContainerBuilderHasParameter('sylius_shop.order_pay.payum_after_pay_route_parameters', []);
}
#[Test]
public function it_configures_order_pay_parameters(): void
{
$this->load([
'order_pay' => [
'payum_after_pay_route' => 'app_shop_order_after_pay',
'payum_after_pay_route_parameters' => ['tokenValue' => 'fixed-token-value'],
],
]);
$this->assertContainerBuilderHasParameter('sylius_shop.order_pay.payum_after_pay_route', 'app_shop_order_after_pay');
$this->assertContainerBuilderHasParameter(
'sylius_shop.order_pay.payum_after_pay_route_parameters',
['tokenValue' => 'fixed-token-value'],
);
}
#[Test]
public function it_prepends_sylius_theme_configuration_with_channel_based_context(): void
{

View file

@ -21,7 +21,7 @@ use Sylius\Component\Core\Model\ProductVariantInterface;
final readonly class ProductVariantLowestPriceMapProvider implements ProductVariantMapProviderInterface
{
public function __construct(
private ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $calculator,
private CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $calculator,
) {
if (!$this->calculator instanceof CatalogPricesCalculatorInterface) {
trigger_deprecation(

View file

@ -21,7 +21,7 @@ use Sylius\Component\Core\Model\ProductVariantInterface;
final class ProductVariantOriginalPriceMapProvider implements ProductVariantMapProviderInterface
{
public function __construct(
private ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $calculator,
private CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $calculator,
) {
if (!$this->calculator instanceof CatalogPricesCalculatorInterface) {
trigger_deprecation(

View file

@ -21,7 +21,7 @@ use Sylius\Component\Core\Model\ProductVariantInterface;
final class ProductVariantPriceMapProvider implements ProductVariantMapProviderInterface
{
public function __construct(
private ProductVariantPricesCalculatorInterface|CatalogPricesCalculatorInterface $calculator,
private CatalogPricesCalculatorInterface|ProductVariantPricesCalculatorInterface $calculator,
) {
if (!$this->calculator instanceof CatalogPricesCalculatorInterface) {
trigger_deprecation(

View file

@ -30,7 +30,7 @@ final class ProductVariantLowestPriceMapProviderTest extends TestCase
private ChannelPricingInterface&MockObject $channelPricing;
private MockObject&CatalogPricesCalculatorInterface $calculator;
private CatalogPricesCalculatorInterface&MockObject $calculator;
private ProductVariantLowestPriceMapProvider $provider;

View file

@ -30,7 +30,7 @@ final class ProductVariantOriginalPriceMapProviderTest extends TestCase
private ChannelPricingInterface&MockObject $channelPricing;
private MockObject&CatalogPricesCalculatorInterface $calculator;
private CatalogPricesCalculatorInterface&MockObject $calculator;
private ProductVariantOriginalPriceMapProvider $provider;

View file

@ -30,7 +30,7 @@ final class ProductVariantPriceMapProviderTest extends TestCase
private ChannelPricingInterface&MockObject $channelPricing;
private MockObject&CatalogPricesCalculatorInterface $calculator;
private CatalogPricesCalculatorInterface&MockObject $calculator;
private ProductVariantPriceMapProvider $provider;