diff --git a/.env b/.env
index 23eeec11589..afdf181c075 100644
--- a/.env
+++ b/.env
@@ -43,3 +43,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=doctrine://default?queue_n
###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###
+
+SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/dev.key
diff --git a/.env.test b/.env.test
index ac192d5cad2..962b635aa8d 100644
--- a/.env.test
+++ b/.env.test
@@ -18,3 +18,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
###< symfony/messenger ###
MAILER_DSN=null://null
+
+SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
diff --git a/.env.test_cached b/.env.test_cached
index e278eea0cc1..12bd367940c 100644
--- a/.env.test_cached
+++ b/.env.test_cached
@@ -19,3 +19,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
###< symfony/messenger ###
MAILER_DSN=null://null
+
+SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
diff --git a/.env.test_cached_payum b/.env.test_cached_payum
index e278eea0cc1..12bd367940c 100644
--- a/.env.test_cached_payum
+++ b/.env.test_cached_payum
@@ -19,3 +19,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
###< symfony/messenger ###
MAILER_DSN=null://null
+
+SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
diff --git a/composer-require-checker.json b/composer-require-checker.json
index a54068ad643..0c4a3dd0929 100644
--- a/composer-require-checker.json
+++ b/composer-require-checker.json
@@ -56,8 +56,11 @@
"HWI\\Bundle\\OAuthBundle\\OAuth\\Response\\UserResponseInterface",
"HWI\\Bundle\\OAuthBundle\\Security\\Core\\User\\OAuthAwareUserProviderInterface",
"League\\Flysystem\\FilesystemOperator",
+ "ParagonIE\\ConstantTime\\Hex",
+ "ParagonIE\\HiddenString\\HiddenString",
"Payum\\Core\\Action\\ActionInterface",
"Payum\\Core\\Action\\GatewayAwareAction",
+ "Payum\\Core\\Bridge\\Doctrine\\Storage\\DoctrineStorage",
"Payum\\Core\\Bridge\\Spl\\ArrayObject",
"Payum\\Core\\Exception\\RequestNotSupportedException",
"Payum\\Core\\Extension\\Context",
diff --git a/composer.json b/composer.json
index 9ccc5f882e8..74247407156 100644
--- a/composer.json
+++ b/composer.json
@@ -30,6 +30,7 @@
"ext-intl": "*",
"ext-json": "*",
"ext-simplexml": "*",
+ "ext-sodium": "*",
"api-platform/core": "^4.0.3",
"babdev/pagerfanta-bundle": "^4.4",
"behat/transliterator": "^1.5",
@@ -59,6 +60,7 @@
"lexik/jwt-authentication-bundle": "^3.1",
"liip/imagine-bundle": "^2.13",
"pagerfanta/pagerfanta": "^4.0",
+ "paragonie/halite": "^5.0",
"payum/offline": "^1.7.5",
"payum/payum-bundle": "^2.6",
"php-http/discovery": "^1.20",
diff --git a/config/encryption/.gitkeep b/config/encryption/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/config/encryption/test.key b/config/encryption/test.key
new file mode 100644
index 00000000000..005987424bc
--- /dev/null
+++ b/config/encryption/test.key
@@ -0,0 +1 @@
+31400500d6649581d6ac178bc41c92acc686dd869e6aa8665b4dad27f8921075e8cbf34059793bf9a0c603cd870f0433fb817afdb68bd75445111f27fe36a3252c8bd26fdbd82801568e9c657b022fd39edabff90518a2e04377e4e813bf3bf7d9411e6e
\ No newline at end of file
diff --git a/src/Sylius/Behat/Context/Cli/InstallerContext.php b/src/Sylius/Behat/Context/Cli/InstallerContext.php
index 6e2ee9fe00c..a8a69d92a12 100644
--- a/src/Sylius/Behat/Context/Cli/InstallerContext.php
+++ b/src/Sylius/Behat/Context/Cli/InstallerContext.php
@@ -57,7 +57,7 @@ public function __construct(
private readonly FactoryInterface $adminUserFactory,
private readonly UserRepositoryInterface $adminUserRepository,
private readonly ValidatorInterface $validator,
- private readonly bool $publicDir,
+ private readonly string $publicDir,
) {
}
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/create.yaml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/create.yaml
index 957786955f7..0c845fdb6d9 100644
--- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/create.yaml
+++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/create.yaml
@@ -44,6 +44,9 @@ sylius_twig_hooks:
'sylius_admin.payment_method.create.content.form.sections.gateway_configuration':
type:
template: '@SyliusAdmin/payment_method/form/sections/gateway_configuration/type.html.twig'
+ priority: 100
+ use_payum:
+ template: '@SyliusAdmin/payment_method/form/sections/gateway_configuration/use_payum.html.twig'
priority: 0
'sylius_admin.payment_method.create.content.form.sections.translations':
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/update.yaml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/update.yaml
index 0e365963097..324df4e5ba6 100644
--- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/update.yaml
+++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/payment_method/update.yaml
@@ -46,6 +46,9 @@ sylius_twig_hooks:
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration':
type:
template: '@SyliusAdmin/payment_method/form/sections/gateway_configuration/type.html.twig'
+ priority: 100
+ use_payum:
+ template: '@SyliusAdmin/payment_method/form/sections/gateway_configuration/use_payum.html.twig'
priority: 0
'sylius_admin.payment_method.update.content.form.sections.translations':
diff --git a/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/type.html.twig b/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/type.html.twig
index 232e5c9b573..d2746f84d22 100644
--- a/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/type.html.twig
+++ b/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/type.html.twig
@@ -1,5 +1,5 @@
{% set form = hookable_metadata.context.form %}
- {{ form_row(form.gatewayConfig.factoryName, sylius_test_form_attribute('factory-name')|merge({ label_attr: { class: 'checkbox-switch' } })) }}
+ {{ form_row(form.gatewayConfig.factoryName, sylius_test_form_attribute('factory-name')) }}
diff --git a/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/use_payum.html.twig b/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/use_payum.html.twig
new file mode 100644
index 00000000000..ddc2ff26b71
--- /dev/null
+++ b/src/Sylius/Bundle/AdminBundle/templates/payment_method/form/sections/gateway_configuration/use_payum.html.twig
@@ -0,0 +1,5 @@
+{% set form = hookable_metadata.context.form %}
+
+
+ {{ form_row(form.gatewayConfig.usePayum, sylius_test_form_attribute('use-payum')|merge({ label_attr: { class: 'checkbox-switch' } })) }}
+
diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php
index 359887473d3..966a5e21d46 100644
--- a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php
+++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php
@@ -46,6 +46,10 @@ final class InstallCommand extends Command
'command' => 'sylius:install:jwt-setup',
'message' => 'Configuring JWT token.',
],
+ [
+ 'command' => 'sylius:payment:generate-key',
+ 'message' => 'Generating payment encryption key.',
+ ],
[
'command' => 'sylius:install:assets',
'message' => 'Installing assets.',
diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php
index 35bbc021942..8b8555bca05 100644
--- a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php
+++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php
@@ -33,7 +33,7 @@ final class InstallSampleDataCommand extends AbstractInstallCommand
public function __construct(
protected readonly EntityManagerInterface $entityManager,
protected readonly CommandDirectoryChecker $commandDirectoryChecker,
- protected readonly bool $publicDir,
+ protected readonly string $publicDir,
) {
parent::__construct($this->entityManager, $this->commandDirectoryChecker);
}
diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174728.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174728.php
new file mode 100644
index 00000000000..4f4b51b327c
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174728.php
@@ -0,0 +1,35 @@
+addSql('ALTER TABLE sylius_gateway_config ADD use_payum TINYINT(1) DEFAULT 1 NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE sylius_gateway_config DROP use_payum');
+ }
+}
diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174729.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174729.php
new file mode 100644
index 00000000000..e21293dc8ed
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20241024174729.php
@@ -0,0 +1,35 @@
+addSql('ALTER TABLE sylius_gateway_config ADD use_payum BOOLEAN DEFAULT true NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE sylius_gateway_config DROP use_payum');
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/Console/Command/GenerateEncryptionKeyCommand.php b/src/Sylius/Bundle/PaymentBundle/Console/Command/GenerateEncryptionKeyCommand.php
new file mode 100644
index 00000000000..da2800b34ff
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Console/Command/GenerateEncryptionKeyCommand.php
@@ -0,0 +1,97 @@
+io = new SymfonyStyle($input, $output);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io->writeln('Generating encryption key for Sylius payment encryption');
+
+ if (false === $input->getOption('overwrite') && $this->filesystem->exists($this->keyPath)) {
+ $this->io->writeln(sprintf('Key file "%s" already exists.', $this->keyPath));
+
+ $answer = $this->io->confirm('Do you want to overwrite it?', false);
+ if (false === $answer) {
+ $this->io->info('Key generation has been canceled');
+
+ return Command::SUCCESS;
+ }
+ }
+
+ try {
+ $generatedKey = KeyFactory::generateEncryptionKey();
+ } catch (CannotPerformOperation|InvalidKey|\TypeError) {
+ $this->io->error('Key could not be generated. Please, make sure that PHP supports libsodium');
+
+ return Command::FAILURE;
+ }
+
+ try {
+ $this->filesystem->mkdir(\dirname($this->keyPath));
+ $this->filesystem->touch($this->keyPath);
+ $saved = KeyFactory::save($generatedKey, $this->keyPath);
+ } catch (IOException) {
+ $saved = false;
+ }
+
+ if (false === $saved) {
+ $this->io->error(sprintf(
+ 'Key could not be saved. Please, make sure that the directory "%s" is writable',
+ \dirname($this->keyPath),
+ ));
+
+ return Command::FAILURE;
+ }
+
+ $this->io->success(sprintf('Key has been generated and saved in "%s"', $this->keyPath));
+
+ return Command::SUCCESS;
+ }
+
+ protected function configure(): void
+ {
+ $this->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrites an existing key file');
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/Configuration.php
index 1f27491516c..4c17a06f666 100644
--- a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/Configuration.php
+++ b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/Configuration.php
@@ -49,6 +49,20 @@ public function getConfigTreeBuilder(): TreeBuilder
$rootNode
->addDefaultsIfNotSet()
->children()
+ ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end()
+ ->arrayNode('encryption')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('enabled')->defaultTrue()->end()
+ ->arrayNode('disabled_for_factories')
+ ->scalarPrototype()->end()
+ ->end()
+ ->end()
+ ->end()
+ ->arrayNode('gateways')
+ ->useAttributeAsKey('name')
+ ->scalarPrototype()->end()
+ ->end()
->arrayNode('gateway_config')
->addDefaultsIfNotSet()
->children()
@@ -66,11 +80,6 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->end()
- ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end()
- ->arrayNode('gateways')
- ->useAttributeAsKey('name')
- ->scalarPrototype()
- ->end()
->end()
;
diff --git a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php
index 4936e50a02f..a60a583b274 100644
--- a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php
+++ b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php
@@ -37,6 +37,8 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('sylius.gateway_config.validation_groups', $config['gateway_config']['validation_groups']);
$container->setParameter('sylius.payment_request.states_to_be_cancelled_when_payment_method_changed', $config['payment_request']['states_to_be_cancelled_when_payment_method_changed']);
+ $this->configureEncryption($config['encryption'], $container);
+
$this->registerAutoconfiguration($container);
}
@@ -73,4 +75,21 @@ static function (ChildDefinition $definition, AsNotifyPaymentProvider $attribute
},
);
}
+
+ /** @param array $encryptionConfig */
+ private function configureEncryption(
+ array $encryptionConfig,
+ ContainerBuilder $container,
+ ): void {
+ $container->setParameter('sylius.encryption.enabled', $encryptionConfig['enabled']);
+ if (false === $encryptionConfig['enabled']) {
+ return;
+ }
+
+ $container->setParameter('sylius.encryption.disabled_for_factories', $encryptionConfig['disabled_for_factories']);
+
+ $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/services/encryption'));
+
+ $loader->load('encryption.xml');
+ }
}
diff --git a/src/Sylius/Bundle/PaymentBundle/Form/Type/GatewayConfigType.php b/src/Sylius/Bundle/PaymentBundle/Form/Type/GatewayConfigType.php
index 349417c0907..7a0076a8004 100644
--- a/src/Sylius/Bundle/PaymentBundle/Form/Type/GatewayConfigType.php
+++ b/src/Sylius/Bundle/PaymentBundle/Form/Type/GatewayConfigType.php
@@ -20,6 +20,7 @@
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
+use Webmozart\Assert\Assert;
final class GatewayConfigType extends AbstractResourceType
{
@@ -33,21 +34,21 @@ public function __construct(
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- $factoryName = $options['data']->getFactoryName();
-
$builder
->add('factoryName', TextType::class, [
'label' => 'sylius.form.gateway_config.type',
'disabled' => true,
- 'data' => $factoryName,
])
- ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($factoryName) {
+ ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$gatewayConfig = $event->getData();
if (!$gatewayConfig instanceof GatewayConfigInterface) {
return;
}
+ $factoryName = $gatewayConfig->getFactoryName();
+ Assert::notNull($factoryName, 'A factory name is required.');
+
if (!$this->gatewayConfigurationTypeRegistry->has('gateway_config', $factoryName)) {
return;
}
diff --git a/src/Sylius/Bundle/PaymentBundle/Listener/EntityEncryptionListener.php b/src/Sylius/Bundle/PaymentBundle/Listener/EntityEncryptionListener.php
new file mode 100644
index 00000000000..65a08ea5356
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Listener/EntityEncryptionListener.php
@@ -0,0 +1,100 @@
+ $entityEncrypter
+ * @param class-string $entityClass
+ */
+ public function __construct(
+ protected readonly EntityEncrypterInterface $entityEncrypter,
+ protected readonly string $entityClass,
+ ) {
+ }
+
+ public function onFlush(OnFlushEventArgs $args): void
+ {
+ $entityManager = $args->getObjectManager();
+ Assert::isInstanceOf($entityManager, EntityManagerInterface::class);
+ $unitOfWork = $entityManager->getUnitOfWork();
+
+ $this->encryptEntities($unitOfWork->getScheduledEntityInsertions(), $entityManager, $unitOfWork);
+ $this->encryptEntities($unitOfWork->getScheduledEntityUpdates(), $entityManager, $unitOfWork);
+ }
+
+ public function postFlush(PostFlushEventArgs $args): void
+ {
+ $entityManager = $args->getObjectManager();
+ Assert::isInstanceOf($entityManager, EntityManagerInterface::class);
+ $unitOfWork = $entityManager->getUnitOfWork();
+
+ /** @var array $entitiesToBeDecrypted */
+ $entitiesToBeDecrypted = $unitOfWork->getIdentityMap()[$this->entityClass] ?? [];
+ if ([] !== $entitiesToBeDecrypted) {
+ $this->decryptEntities($entitiesToBeDecrypted);
+ }
+ }
+
+ public function postLoad(PostLoadEventArgs $args): void
+ {
+ $this->decryptEntities([$args->getObject()]);
+ }
+
+ /** @param array $entities */
+ protected function encryptEntities(array $entities, EntityManagerInterface $entityManager, UnitOfWork $unitOfWork): void
+ {
+ foreach ($entities as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $this->entityEncrypter->encrypt($entity);
+ $metadata = $entityManager->getClassMetadata(get_class($entity));
+ $unitOfWork->recomputeSingleEntityChangeSet($metadata, $entity);
+ }
+ }
+
+ /** @param array $entities */
+ protected function decryptEntities(array $entities): void
+ {
+ foreach ($entities as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $this->entityEncrypter->decrypt($entity);
+ }
+ }
+
+ protected function supports(mixed $entity): bool
+ {
+ return is_a($entity, $this->entityClass, true);
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/Listener/GatewayConfigEncryptionListener.php b/src/Sylius/Bundle/PaymentBundle/Listener/GatewayConfigEncryptionListener.php
new file mode 100644
index 00000000000..acdc1aaa829
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Listener/GatewayConfigEncryptionListener.php
@@ -0,0 +1,46 @@
+
+ *
+ * @experimental
+ */
+final class GatewayConfigEncryptionListener extends EntityEncryptionListener
+{
+ /**
+ * @param EntityEncrypterInterface $entityEncrypter
+ * @param class-string $entityClass
+ * @param array $disabledGatewayFactories
+ */
+ public function __construct(
+ EntityEncrypterInterface $entityEncrypter,
+ string $entityClass,
+ private readonly array $disabledGatewayFactories,
+ ) {
+ parent::__construct($entityEncrypter, $entityClass);
+ }
+
+ protected function supports(mixed $entity): bool
+ {
+ return
+ parent::supports($entity) &&
+ !in_array($entity->getFactoryName(), $this->disabledGatewayFactories, true)
+ ;
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml
index 3d09785c17f..bd431040c23 100644
--- a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml
+++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml
@@ -4,6 +4,9 @@
imports:
- { resource: "@SyliusPaymentBundle/Resources/config/app/messenger.yaml" }
+parameters:
+ env(SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH): '%kernel.project_dir%/config/encryption/key'
+
sylius_payment:
payment_request:
states_to_be_cancelled_when_payment_method_changed:
diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/services.xml b/src/Sylius/Bundle/PaymentBundle/Resources/config/services.xml
index 75dc215d890..56331f35cda 100644
--- a/src/Sylius/Bundle/PaymentBundle/Resources/config/services.xml
+++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/services.xml
@@ -17,7 +17,7 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
-
+
diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/services/console_command.xml b/src/Sylius/Bundle/PaymentBundle/Resources/config/services/console_command.xml
new file mode 100644
index 00000000000..782323aed77
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/services/console_command.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+ %env(resolve:SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH)%
+
+
+
+
diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/services/encryption/encryption.xml b/src/Sylius/Bundle/PaymentBundle/Resources/config/services/encryption/encryption.xml
new file mode 100644
index 00000000000..db83ca6ec1f
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/services/encryption/encryption.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ %env(resolve:SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH)%
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %sylius.model.gateway_config.class%
+ %sylius.encryption.disabled_for_factories%
+
+
+
+
+
+
+
+ %sylius.model.payment_request.class%
+
+
+
+
+
+
diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/PaymentBundle/Resources/translations/messages.en.yml
index a3aeacdb69f..e51cb4a09d8 100644
--- a/src/Sylius/Bundle/PaymentBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/PaymentBundle/Resources/translations/messages.en.yml
@@ -32,5 +32,7 @@ sylius:
instructions: Instructions
name: Name
position: Position
+ gateway_config:
+ type: Type
gateway_factory:
offline: Offline
diff --git a/src/Sylius/Bundle/PaymentBundle/Tests/Console/Command/GenerateEncryptionKeyCommandTest.php b/src/Sylius/Bundle/PaymentBundle/Tests/Console/Command/GenerateEncryptionKeyCommandTest.php
new file mode 100644
index 00000000000..9568c65d9f0
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Tests/Console/Command/GenerateEncryptionKeyCommandTest.php
@@ -0,0 +1,122 @@
+boot();
+
+ $command = new GenerateEncryptionKeyCommand(new Filesystem(), self::ENCRYPTION_KEY_PATH);
+
+ $this->commandTester = new CommandTester($command);
+ }
+
+ /** @test */
+ public function it_generates_and_saves_the_encryption_key_in_path(): void
+ {
+ $this->commandTester->execute([]);
+
+ $output = $this->commandTester->getDisplay();
+
+ $this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
+
+ $this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
+ $this->assertStringContainsString('Key has been generated and saved in', $output);
+ $this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
+ }
+
+ /** @test */
+ public function it_does_not_overwrite_existing_key_when_it_is_not_requested(): void
+ {
+ $this->commandTester->setInputs(['Do you want to overwrite it?' => 'n']);
+ $this->commandTester->execute([]);
+
+ $output = $this->commandTester->getDisplay();
+
+ $this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
+
+ $this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
+ $this->assertStringContainsString('Do you want to overwrite it? (yes/no)', $output);
+ $this->assertStringContainsString(
+ $this->normalizeString(sprintf('"%s" already exists', self::ENCRYPTION_KEY_PATH)),
+ $this->normalizeString($output),
+ );
+ $this->assertStringContainsString('[INFO] Key generation has been canceled', $output);
+ }
+
+ /** @test */
+ public function it_overwrites_existing_key_when_requested(): void
+ {
+ $this->commandTester->setInputs(['Do you want to overwrite it?' => 'y']);
+ $this->commandTester->execute([]);
+
+ $output = $this->commandTester->getDisplay();
+
+ $this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
+
+ $this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
+ $this->assertStringContainsString('Do you want to overwrite it? (yes/no)', $output);
+ $this->assertStringContainsString(
+ $this->normalizeString(sprintf('"%s" already exists', self::ENCRYPTION_KEY_PATH)),
+ $this->normalizeString($output),
+ );
+ $this->assertStringContainsString('Key has been generated and saved in', $output);
+ $this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
+ }
+
+ /** @test */
+ public function it_automatically_overwrites_existing_key_when_overwrite_option_is_passed(): void
+ {
+ $this->commandTester->execute(['--overwrite' => true]);
+
+ $output = $this->commandTester->getDisplay();
+
+ $this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
+
+ $this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
+ $this->assertStringContainsString('Key has been generated and saved in', $output);
+ $this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ self::removeKey();
+ }
+
+ private static function removeKey(): void
+ {
+ if (file_exists(self::ENCRYPTION_KEY_PATH)) {
+ unlink(self::ENCRYPTION_KEY_PATH);
+ rmdir(dirname(self::ENCRYPTION_KEY_PATH));
+ }
+ }
+
+ private function normalizeString(string $string): string
+ {
+ return preg_replace('/\s+/', '', $string);
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/ConfigurationTest.php
new file mode 100644
index 00000000000..4348e9b9765
--- /dev/null
+++ b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -0,0 +1,68 @@
+assertProcessedConfigurationEquals(
+ [[]],
+ ['encryption' => ['enabled' => true]],
+ 'encryption.enabled',
+ );
+ }
+
+ /** @test */
+ public function its_encryption_can_be_turned_off(): void
+ {
+ $this->assertProcessedConfigurationEquals(
+ [['encryption' => ['enabled' => false]]],
+ ['encryption' => ['enabled' => false]],
+ 'encryption.enabled',
+ );
+ }
+
+ /** @test */
+ public function it_treats_null_like_true_in_gateways_encryption_configuration(): void
+ {
+ $this->assertProcessedConfigurationEquals(
+ [['encryption' => ['disabled_for_factories' => ['offline']]]],
+ ['encryption' => ['disabled_for_factories' => ['offline']]],
+ 'encryption.disabled_for_factories',
+ );
+ }
+
+ /** @test */
+ public function it_can_configure_not_encrypted_gateways(): void
+ {
+ $this->assertProcessedConfigurationEquals(
+ [['encryption' => ['disabled_for_factories' => ['offline']]]],
+ ['encryption' => ['disabled_for_factories' => ['offline']]],
+ 'encryption.disabled_for_factories',
+ );
+ }
+
+ protected function getConfiguration(): Configuration
+ {
+ return new Configuration();
+ }
+}
diff --git a/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php
index 987b2e215f1..c45e20b88be 100644
--- a/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php
+++ b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php
@@ -122,6 +122,51 @@ public function it_loads_parameter_with_payment_request_states_that_should_be_ca
);
}
+ /** @test */
+ public function it_loads_encryption_services_when_encryption_is_enabled(): void
+ {
+ $this->load([
+ 'encryption' => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $this->assertContainerBuilderHasParameter('sylius.encryption.enabled', true);
+ $this->assertContainerBuilderHasParameter('sylius.encryption.disabled_for_factories', []);
+
+ $this->compile();
+
+ $this->assertContainerBuilderHasService('sylius.encrypter');
+ }
+
+ /** @test */
+ public function it_populates_encryption_disabled_for_factories_parameter(): void
+ {
+ $this->load([
+ 'encryption' => [
+ 'disabled_for_factories' => ['paypal_express_checkout'],
+ ],
+ ]);
+
+ $this->assertContainerBuilderHasParameter('sylius.encryption.disabled_for_factories', ['paypal_express_checkout']);
+ }
+
+ /** @test */
+ public function it_does_not_load_encryption_services_when_encryption_is_disabled(): void
+ {
+ $this->load([
+ 'encryption' => [
+ 'enabled' => false,
+ ],
+ ]);
+
+ $this->assertContainerBuilderHasParameter('sylius.encryption.enabled', false);
+
+ $this->compile();
+
+ $this->assertContainerBuilderNotHasService('sylius.encrypter');
+ }
+
protected function getContainerExtensions(): array
{
return [new SyliusPaymentExtension()];
diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php
index 9c24202d600..df552eb473d 100644
--- a/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php
+++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php
@@ -50,18 +50,21 @@ private function prependSyliusPayment(ContainerBuilder $container): void
}
$gateways = [];
+ $gatewayFactories = [];
$configs = $container->getExtensionConfig('payum');
foreach ($configs as $config) {
if (!isset($config['gateways'])) {
continue;
}
-
- /** @var string $gatewayKey */
- foreach (array_keys($config['gateways']) as $gatewayKey) {
+ foreach ($config['gateways'] as $gatewayKey => $gatewayConfig) {
$gateways[$gatewayKey] = 'sylius.payum_gateway.' . $gatewayKey;
+ $gatewayFactories[] = $gatewayConfig['factory'] ?? null;
}
}
$container->prependExtensionConfig('sylius_payment', ['gateways' => $gateways]);
+ $container->prependExtensionConfig('sylius_payment', ['encryption' => [
+ 'disabled_for_factories' => array_filter($gatewayFactories),
+ ]]);
}
}
diff --git a/src/Sylius/Bundle/PayumBundle/Form/Extension/PayumGatewayConfigTypeExtension.php b/src/Sylius/Bundle/PayumBundle/Form/Extension/PayumGatewayConfigTypeExtension.php
new file mode 100644
index 00000000000..95fa5b120a4
--- /dev/null
+++ b/src/Sylius/Bundle/PayumBundle/Form/Extension/PayumGatewayConfigTypeExtension.php
@@ -0,0 +1,73 @@
+addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
+ $gatewayConfig = $event->getData();
+
+ if (!$gatewayConfig instanceof GatewayConfigInterface) {
+ return;
+ }
+
+ $factoryName = $gatewayConfig->getFactoryName();
+ Assert::notNull($factoryName, 'A factory name is required.');
+
+ // Check if a Payum factory exists
+ $supportPayum = isset($this->payum->getGatewayFactories()[$factoryName]);
+
+ if(!$supportPayum) {
+ $gatewayConfig->setUsePayum(false);
+ }
+
+ // Check if PaymentRequest exists
+ $supportPaymentRequest = null !== $this->gatewayFactoryCommandProvider->getCommandProvider($factoryName);
+
+ $event->getForm()->add('usePayum', CheckboxType::class, [
+ 'required' => false,
+ 'label' => 'sylius.form.gateway_config.use_payum',
+ 'disabled' => !($supportPayum && $supportPaymentRequest),
+ ]);
+
+ $event->setData($gatewayConfig);
+ })
+ ;
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [GatewayConfigType::class];
+ }
+}
diff --git a/src/Sylius/Bundle/PayumBundle/Model/GatewayConfig.php b/src/Sylius/Bundle/PayumBundle/Model/GatewayConfig.php
index 697a585f74a..fe541a99a42 100644
--- a/src/Sylius/Bundle/PayumBundle/Model/GatewayConfig.php
+++ b/src/Sylius/Bundle/PayumBundle/Model/GatewayConfig.php
@@ -20,6 +20,7 @@ class GatewayConfig extends BaseGatewayConfig implements GatewayConfigInterface
{
/** @var array */
protected array $decryptedConfig;
+ protected bool $usePayum = true;
public function __construct()
{
@@ -28,6 +29,16 @@ public function __construct()
$this->decryptedConfig = [];
}
+ public function getUsePayum(): bool
+ {
+ return $this->usePayum;
+ }
+
+ public function setUsePayum(bool $usePayum): void
+ {
+ $this->usePayum = $usePayum;
+ }
+
public function getConfig(): array
{
if (isset($this->config['encrypted'])) {
diff --git a/src/Sylius/Bundle/PayumBundle/Model/GatewayConfigInterface.php b/src/Sylius/Bundle/PayumBundle/Model/GatewayConfigInterface.php
index 9e779d9c041..ff50cd39dc2 100644
--- a/src/Sylius/Bundle/PayumBundle/Model/GatewayConfigInterface.php
+++ b/src/Sylius/Bundle/PayumBundle/Model/GatewayConfigInterface.php
@@ -19,4 +19,7 @@
interface GatewayConfigInterface extends BaseGatewayConfigInterface, PayumGatewayConfigInterface, CryptedInterface
{
+ public function getUsePayum(): bool;
+
+ public function setUsePayum(bool $usePayum): void;
}
diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/doctrine/model/GatewayConfig.orm.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/doctrine/model/GatewayConfig.orm.xml
index 3bc6a44072f..44dd28b324d 100644
--- a/src/Sylius/Bundle/PayumBundle/Resources/config/doctrine/model/GatewayConfig.orm.xml
+++ b/src/Sylius/Bundle/PayumBundle/Resources/config/doctrine/model/GatewayConfig.orm.xml
@@ -16,5 +16,11 @@
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"
>
-
+
+
+
+
+
+
+
diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/services/form.xml
index abb1f5ee512..68172f15473 100644
--- a/src/Sylius/Bundle/PayumBundle/Resources/config/services/form.xml
+++ b/src/Sylius/Bundle/PayumBundle/Resources/config/services/form.xml
@@ -17,6 +17,11 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
+
+
+
+
+
diff --git a/src/Sylius/Bundle/PayumBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/PayumBundle/Resources/translations/messages.en.yml
index d5eb234be3b..89a5cfd759d 100644
--- a/src/Sylius/Bundle/PayumBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/PayumBundle/Resources/translations/messages.en.yml
@@ -1,9 +1,9 @@
sylius:
form:
gateway_config:
- type: Type
+ use_payum: Use Payum
payum_gateway:
cash_on_delivery: Cash on delivery
- offline: Offline
+ offline: Offline (Payum)
payum_gateway_factory:
offline: Offline
diff --git a/src/Sylius/Bundle/PayumBundle/Storage/DoctrineStorage.php b/src/Sylius/Bundle/PayumBundle/Storage/DoctrineStorage.php
index a269c959e45..3ceb69a058a 100644
--- a/src/Sylius/Bundle/PayumBundle/Storage/DoctrineStorage.php
+++ b/src/Sylius/Bundle/PayumBundle/Storage/DoctrineStorage.php
@@ -13,55 +13,38 @@
namespace Sylius\Bundle\PayumBundle\Storage;
-use Doctrine\Persistence\ObjectManager;
-use Payum\Core\Model\Identity;
-use Payum\Core\Storage\AbstractStorage;
+use Payum\Core\Bridge\Doctrine\Storage\DoctrineStorage as BaseDoctrineStorage;
+use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface;
-/**
- * It's a drop-in replacement for DoctrineStorage that accepts
- * Doctrine\Persistence\ObjectManager instead of Doctrine\Common\Persistence\ObjectManager.
- *
- * @internal
- *
- * @see \Payum\Core\Bridge\Doctrine\Storage\DoctrineStorage
- */
-class DoctrineStorage extends AbstractStorage
+class DoctrineStorage extends BaseDoctrineStorage
{
- public function __construct(protected ObjectManager $objectManager, $modelClass)
+ protected function doFind($id): ?object
{
- parent::__construct($modelClass);
- }
+ /** @var object|GatewayConfigInterface|null $resource */
+ $resource = parent::doFind($id);
- public function findBy(array $criteria): array
- {
- return $this->objectManager->getRepository($this->modelClass)->findBy($criteria);
- }
+ if (null === $resource) {
+ return null;
+ }
- protected function doFind($id): ?object
- {
- return $this->objectManager->find($this->modelClass, $id);
- }
+ if (!$resource instanceof GatewayConfigInterface) {
+ return $resource;
+ }
- protected function doUpdateModel($model): void
- {
- $this->objectManager->persist($model);
- $this->objectManager->flush();
+ return $resource->getUsePayum() ? $resource : null;
}
- protected function doDeleteModel($model): void
+ public function findBy(array $criteria): array
{
- $this->objectManager->remove($model);
- $this->objectManager->flush();
- }
+ /** @var object[]|GatewayConfigInterface[] $resources */
+ $resources = parent::findBy($criteria);
- protected function doGetIdentity($model): Identity
- {
- $modelMetadata = $this->objectManager->getClassMetadata($model::class);
- $id = $modelMetadata->getIdentifierValues($model);
- if (count($id) > 1) {
- throw new \LogicException('Storage not support composite primary ids');
- }
+ return array_filter($resources, static function($resource) {
+ if (!$resource instanceof GatewayConfigInterface) {
+ return true;
+ }
- return new Identity(array_shift($id), $model);
+ return $resource->getUsePayum();
+ });
}
}
diff --git a/src/Sylius/Component/Payment/Encryption/Encrypter.php b/src/Sylius/Component/Payment/Encryption/Encrypter.php
new file mode 100644
index 00000000000..c0a5bd2763e
--- /dev/null
+++ b/src/Sylius/Component/Payment/Encryption/Encrypter.php
@@ -0,0 +1,75 @@
+getKey()) . self::ENCRYPTION_SUFFIX;
+ } catch (HaliteAlert|\SodiumException|\TypeError $exception) {
+ throw EncryptionException::cannotEncrypt($exception);
+ }
+ }
+
+ public function decrypt(string $data): string
+ {
+ if (!str_ends_with($data, self::ENCRYPTION_SUFFIX)) {
+ return $data;
+ }
+
+ try {
+ $data = substr($data, 0, -self::ENCRYPTION_SUFFIX_LENGTH);
+
+ return Crypto::decrypt($data, $this->getKey())->getString();
+ } catch (HaliteAlert|\SodiumException|\TypeError $exception) {
+ throw EncryptionException::cannotDecrypt($exception);
+ }
+ }
+
+ private function getKey(): EncryptionKey
+ {
+ if (null === $this->key) {
+ try {
+ $this->key = KeyFactory::loadEncryptionKey($this->encryptionKeyPath);
+ } catch (CannotPerformOperation|InvalidKey $exception) {
+ throw EncryptionException::invalidKey($exception);
+ }
+ }
+
+ return $this->key;
+ }
+}
diff --git a/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php b/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php
new file mode 100644
index 00000000000..7572af98e9e
--- /dev/null
+++ b/src/Sylius/Component/Payment/Encryption/EncrypterInterface.php
@@ -0,0 +1,26 @@
+
+ *
+ * @experimental
+ */
+final readonly class GatewayConfigEncrypter implements EntityEncrypterInterface
+{
+ public function __construct(
+ private EncrypterInterface $encrypter,
+ ) {
+ }
+
+ public function encrypt(EncryptionAwareInterface $resource): void
+ {
+ $encryptedConfig = [];
+ foreach ($resource->getConfig() as $key => $value) {
+ $encryptedConfig[$key] = $this->encrypter->encrypt(serialize($value));
+ }
+
+ $resource->setConfig($encryptedConfig);
+ }
+
+ public function decrypt(EncryptionAwareInterface $resource): void
+ {
+ $decryptedConfig = [];
+ foreach ($resource->getConfig() as $key => $value) {
+ $decryptedConfig[$key] = unserialize($this->encrypter->decrypt($value));
+ }
+
+ $resource->setConfig($decryptedConfig);
+ }
+}
diff --git a/src/Sylius/Component/Payment/Encryption/PaymentRequestEncrypter.php b/src/Sylius/Component/Payment/Encryption/PaymentRequestEncrypter.php
new file mode 100644
index 00000000000..83e69ba65d6
--- /dev/null
+++ b/src/Sylius/Component/Payment/Encryption/PaymentRequestEncrypter.php
@@ -0,0 +1,57 @@
+
+ *
+ * @experimental
+ */
+final readonly class PaymentRequestEncrypter implements EntityEncrypterInterface
+{
+ public function __construct(
+ private EncrypterInterface $encrypter,
+ ) {
+ }
+
+ public function encrypt(EncryptionAwareInterface $resource): void
+ {
+ if (null !== $resource->getPayload()) {
+ $resource->setPayload($this->encrypter->encrypt(serialize($resource->getPayload())));
+ }
+
+ $encryptedRequestData = [];
+ foreach ($resource->getResponseData() as $key => $value) {
+ $encryptedRequestData[$key] = $this->encrypter->encrypt(serialize($value));
+ }
+
+ $resource->setResponseData($encryptedRequestData);
+ }
+
+ public function decrypt(EncryptionAwareInterface $resource): void
+ {
+ if (null !== $resource->getPayload()) {
+ $resource->setPayload(unserialize($this->encrypter->decrypt($resource->getPayload())));
+ }
+
+ $decryptedRequestData = [];
+ foreach ($resource->getResponseData() as $key => $value) {
+ $decryptedRequestData[$key] = unserialize($this->encrypter->decrypt($value));
+ }
+
+ $resource->setResponseData($decryptedRequestData);
+ }
+}
diff --git a/src/Sylius/Component/Payment/Model/GatewayConfigInterface.php b/src/Sylius/Component/Payment/Model/GatewayConfigInterface.php
index 24cad88df8a..67b016e6716 100644
--- a/src/Sylius/Component/Payment/Model/GatewayConfigInterface.php
+++ b/src/Sylius/Component/Payment/Model/GatewayConfigInterface.php
@@ -13,9 +13,10 @@
namespace Sylius\Component\Payment\Model;
+use Sylius\Component\Payment\Encryption\EncryptionAwareInterface;
use Sylius\Component\Resource\Model\ResourceInterface;
-interface GatewayConfigInterface extends ResourceInterface
+interface GatewayConfigInterface extends ResourceInterface, EncryptionAwareInterface
{
public function getGatewayName(): ?string;
diff --git a/src/Sylius/Component/Payment/Model/PaymentRequestInterface.php b/src/Sylius/Component/Payment/Model/PaymentRequestInterface.php
index a098f8e5a7b..4364edba21b 100644
--- a/src/Sylius/Component/Payment/Model/PaymentRequestInterface.php
+++ b/src/Sylius/Component/Payment/Model/PaymentRequestInterface.php
@@ -13,12 +13,13 @@
namespace Sylius\Component\Payment\Model;
+use Sylius\Component\Payment\Encryption\EncryptionAwareInterface;
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Component\Resource\Model\TimestampableInterface;
use Symfony\Component\Uid\Uuid;
/** @experimental */
-interface PaymentRequestInterface extends TimestampableInterface, ResourceInterface
+interface PaymentRequestInterface extends TimestampableInterface, ResourceInterface, EncryptionAwareInterface
{
public const STATE_CANCELLED = 'cancelled';
diff --git a/src/Sylius/Component/Payment/composer.json b/src/Sylius/Component/Payment/composer.json
index f52a2fde28b..538dcc450e5 100644
--- a/src/Sylius/Component/Payment/composer.json
+++ b/src/Sylius/Component/Payment/composer.json
@@ -27,6 +27,7 @@
],
"require": {
"php": "^8.2",
+ "paragonie/halite": "^5.0",
"sylius/registry": "^1.6",
"sylius/resource": "^1.12",
"symfony/uid": "^6.4 || ^7.1"
diff --git a/src/Sylius/Component/Payment/spec/Encryption/EncrypterSpec.php b/src/Sylius/Component/Payment/spec/Encryption/EncrypterSpec.php
new file mode 100644
index 00000000000..a25912e44c6
--- /dev/null
+++ b/src/Sylius/Component/Payment/spec/Encryption/EncrypterSpec.php
@@ -0,0 +1,64 @@
+beConstructedWith(__DIR__ . '/fixtures/encryption_key');
+ }
+
+ function it_is_an_encrypter(): void
+ {
+ $this->shouldImplement(EncrypterInterface::class);
+ }
+
+ function it_throws_an_exception_if_it_cannot_encrypt(): void
+ {
+ $this->beConstructedWith('');
+ $this->shouldThrow(EncryptionException::class)->during('encrypt', ['data']);
+ }
+
+ function it_throws_an_exception_if_it_cannot_decrypt(): void
+ {
+ $this->beConstructedWith('');
+ $this->shouldThrow(EncryptionException::class)->during('decrypt', ['data#ENCRYPTED']);
+ }
+
+ function it_encrypts_data(): void
+ {
+ $this->encrypt('data')->shouldBeString();
+ $this->encrypt('data')->shouldNotBe('data');
+ $this->encrypt('data')->shouldEndWith('#ENCRYPTED');
+ }
+
+ function it_decrypts_data(): void
+ {
+ $data = 'data';
+ $encryptedData = $this->getWrappedObject()->encrypt($data);
+
+ $this->decrypt($encryptedData)->shouldNotEndWith('#ENCRYPTED');
+ $this->decrypt($encryptedData)->shouldBe($data);
+ }
+
+ function it_does_nothing_when_data_is_not_marked_as_encrypted(): void
+ {
+ $this->decrypt('data')->shouldBe('data');
+ }
+}
diff --git a/src/Sylius/Component/Payment/spec/Encryption/GatewayConfigEncrypterSpec.php b/src/Sylius/Component/Payment/spec/Encryption/GatewayConfigEncrypterSpec.php
new file mode 100644
index 00000000000..14c17557500
--- /dev/null
+++ b/src/Sylius/Component/Payment/spec/Encryption/GatewayConfigEncrypterSpec.php
@@ -0,0 +1,111 @@
+beConstructedWith($encrypter);
+ }
+
+ function it_is_an_entity_encrypter(): void
+ {
+ $this->shouldImplement(EntityEncrypterInterface::class);
+ }
+
+ function it_does_nothing_when_encrypting_empty_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn([]);
+
+ $encrypter->encrypt(Argument::any())->shouldNotBeCalled();
+
+ $gatewayConfig->setConfig([])->shouldBeCalled();
+
+ $this->encrypt($gatewayConfig);
+ }
+
+ function it_encrypts_scalar_values_in_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn(['key' => 'value']);
+
+ $encrypter->encrypt(serialize('value'))->willReturn('encrypted_value');
+
+ $gatewayConfig->setConfig(['key' => 'encrypted_value'])->shouldBeCalled();
+
+ $this->encrypt($gatewayConfig);
+ }
+
+ function it_encrypts_array_values_in_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn(['key' => ['value', 'some_other_value']]);
+
+ $encrypter->encrypt(serialize(['value', 'some_other_value']))->willReturn('encrypted_value');
+
+ $gatewayConfig->setConfig(['key' => 'encrypted_value'])->shouldBeCalled();
+
+ $this->encrypt($gatewayConfig);
+ }
+
+ function it_does_nothing_when_decrypting_empty_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn([]);
+
+ $encrypter->decrypt(Argument::any())->shouldNotBeCalled();
+
+ $gatewayConfig->setConfig([])->shouldBeCalled();
+
+ $this->decrypt($gatewayConfig);
+ }
+
+ function it_decrypts_scalar_values_in_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn(['key' => 'encrypted_value']);
+
+ $encrypter->decrypt('encrypted_value')->willReturn(serialize('value'));
+
+ $gatewayConfig->setConfig(['key' => 'value'])->shouldBeCalled();
+
+ $this->decrypt($gatewayConfig);
+ }
+
+ function it_decrypts_array_values_in_gateway_config(
+ EncrypterInterface $encrypter,
+ GatewayConfigInterface $gatewayConfig,
+ ): void {
+ $gatewayConfig->getConfig()->willReturn(['key' => 'encrypted_value']);
+
+ $encrypter->decrypt('encrypted_value')->willReturn(serialize(['value', 'some_other_value']));
+
+ $gatewayConfig->setConfig(['key' => ['value', 'some_other_value']])->shouldBeCalled();
+
+ $this->decrypt($gatewayConfig);
+ }
+}
diff --git a/src/Sylius/Component/Payment/spec/Encryption/PaymentRequestEncrypterSpec.php b/src/Sylius/Component/Payment/spec/Encryption/PaymentRequestEncrypterSpec.php
new file mode 100644
index 00000000000..9a079fc8bd8
--- /dev/null
+++ b/src/Sylius/Component/Payment/spec/Encryption/PaymentRequestEncrypterSpec.php
@@ -0,0 +1,216 @@
+beConstructedWith($encrypter);
+ }
+
+ function it_is_an_entity_encrypter(): void
+ {
+ $this->shouldImplement(EntityEncrypterInterface::class);
+ }
+
+ function it_does_nothing_when_encrypting_payment_request_with_no_payload_and_empty_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->encrypt(Argument::any())->shouldNotBeCalled();
+
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_encrypts_scalar_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn('payload');
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->encrypt(serialize('payload'))->willReturn('encrypted_payload');
+
+ $paymentRequest->setPayload('encrypted_payload')->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_encrypts_array_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(['key' => 'value']);
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->encrypt(serialize(['key' => 'value']))->willReturn('encrypted_payload');
+
+ $paymentRequest->setPayload('encrypted_payload')->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_encrypts_object_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $object = new \stdClass();
+
+ $paymentRequest->getPayload()->willReturn($object);
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->encrypt(serialize($object))->willReturn('encrypted_payload');
+
+ $paymentRequest->setPayload('encrypted_payload')->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_encrypts_scalar_values_in_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn(['key' => 'value']);
+
+ $encrypter->encrypt(serialize('value'))->willReturn('encrypted_value');
+
+ $paymentRequest->setPayload(null)->shouldNotBeCalled();
+ $paymentRequest->setResponseData(['key' => 'encrypted_value'])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_encrypts_array_values_in_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn(['key' => ['value', 'some_other_value']]);
+
+ $encrypter->encrypt(serialize(['value', 'some_other_value']))->willReturn('encrypted_value');
+
+ $paymentRequest->setPayload(null)->shouldNotBeCalled();
+ $paymentRequest->setResponseData(['key' => 'encrypted_value'])->shouldBeCalled();
+
+ $this->encrypt($paymentRequest);
+ }
+
+ function it_does_nothing_when_decrypting_payment_request_with_no_payload_and_empty_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->decrypt(Argument::any())->shouldNotBeCalled();
+
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+
+ function it_decrypts_scalar_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn('encrypted_payload');
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->decrypt('encrypted_payload')->willReturn(serialize('payload'));
+
+ $paymentRequest->setPayload('payload')->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+
+ function it_decrypts_array_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn('encrypted_payload');
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $encrypter->decrypt('encrypted_payload')->willReturn(serialize(['key' => 'value']));
+
+ $paymentRequest->setPayload(['key' => 'value'])->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+
+ function it_decrypts_object_payload(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn('encrypted_payload');
+ $paymentRequest->getResponseData()->willReturn([]);
+
+ $object = new \stdClass();
+
+ $encrypter->decrypt('encrypted_payload')->willReturn(serialize($object));
+
+ $paymentRequest->setPayload($object)->shouldBeCalled();
+ $paymentRequest->setResponseData([])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+
+ function it_decrypts_scalar_values_in_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn(['key' => 'encrypted_value']);
+
+ $encrypter->decrypt('encrypted_value')->willReturn(serialize('value'));
+
+ $paymentRequest->setPayload(null)->shouldNotBeCalled();
+ $paymentRequest->setResponseData(['key' => 'value'])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+
+ function it_decrypts_array_values_in_response_data(
+ PaymentRequestInterface $paymentRequest,
+ EncrypterInterface $encrypter,
+ ): void {
+ $paymentRequest->getPayload()->willReturn(null);
+ $paymentRequest->getResponseData()->willReturn(['key' => 'encrypted_value']);
+
+ $encrypter->decrypt('encrypted_value')->willReturn(serialize(['value', 'some_other_value']));
+
+ $paymentRequest->setPayload(null)->shouldNotBeCalled();
+ $paymentRequest->setResponseData(['key' => ['value', 'some_other_value']])->shouldBeCalled();
+
+ $this->decrypt($paymentRequest);
+ }
+}
diff --git a/src/Sylius/Component/Payment/spec/Encryption/fixtures/encryption_key b/src/Sylius/Component/Payment/spec/Encryption/fixtures/encryption_key
new file mode 100644
index 00000000000..0ec9fbbd6ac
--- /dev/null
+++ b/src/Sylius/Component/Payment/spec/Encryption/fixtures/encryption_key
@@ -0,0 +1 @@
+31400500d37bed69c4fc80633efe2724978b6304197ba4bb15b895a36cc9ef2c833e0ca107738307224758566d75e5f3bf023329caaf360793f91f376ca5d0ac6bbe937e024a8b328ac4fd79649213537f7e377f6da41f10db8f8d7d5c2f52e80412e9f3
\ No newline at end of file
diff --git a/tests/Functional/Encryption/GatewayConfigEncryptionTest.php b/tests/Functional/Encryption/GatewayConfigEncryptionTest.php
new file mode 100644
index 00000000000..20ed97c7061
--- /dev/null
+++ b/tests/Functional/Encryption/GatewayConfigEncryptionTest.php
@@ -0,0 +1,155 @@
+ [
+ 'pk' => 'test',
+ 'sk' => 'test',
+ 'url' => 'https://example.com',
+ ],
+ 'signature' => 'test',
+ 'merchant_id' => 'test',
+ ];
+
+ private EntityManagerInterface $entityManager;
+
+ /** @var RepositoryInterface */
+ private RepositoryInterface $gatewayConfigRepository;
+
+ /** @var FactoryInterface */
+ private FactoryInterface $gatewayFactory;
+
+ public function setUp(): void
+ {
+ $this->entityManager = self::getContainer()->get('doctrine.orm.default_entity_manager');
+ $this->gatewayConfigRepository = self::getContainer()->get('sylius.repository.gateway_config');
+ $this->gatewayFactory = self::getContainer()->get('sylius.factory.gateway_config');
+
+ $encrypter = self::getContainer()->get('sylius.encrypter.gateway_config');
+ self::getContainer()->set('sylius.listener.gateway_config_encryption', new GatewayConfigEncryptionListener(
+ $encrypter,
+ 'Sylius\Bundle\PayumBundle\Model\GatewayConfig',
+ ['online-disabled'],
+ ));
+
+ $this->loadFixtures([
+ __DIR__ . '/../../DataFixtures/ORM/resources/channels.yml',
+ ]);
+ }
+
+ /** @test */
+ public function it_covers_encryption_and_decryption_when_saving_and_loading(): void
+ {
+ $gatewayConfig = $this->gatewayFactory->createNew();
+ $gatewayConfig->setGatewayName('Online');
+ $gatewayConfig->setFactoryName('online');
+ $gatewayConfig->setConfig(self::$gatewayConfigData);
+
+ $this->gatewayConfigRepository->add($gatewayConfig);
+ self::assertSame(self::$gatewayConfigData, $gatewayConfig->getConfig());
+
+ $this->entityManager->clear();
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('Online');
+ self::assertNotSame($gatewayConfig->getConfig(), $gatewayConfigFromDatabase);
+
+ $gatewayFromRepository = $this->gatewayConfigRepository->findOneBy(['gatewayName' => 'Online']);
+ self::assertSame($gatewayConfig->getConfig(), $gatewayFromRepository->getConfig());
+ self::assertSame(self::$gatewayConfigData, $gatewayConfig->getConfig());
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('Online');
+ self::assertNotSame($gatewayConfig->getConfig(), $gatewayConfigFromDatabase);
+ self::assertNotSame(self::$gatewayConfigData, $gatewayConfigFromDatabase);
+ }
+
+ /** @test */
+ public function it_does_not_encrypt_when_gateway_factory_is_disabled_for_encryption(): void
+ {
+ $gatewayConfig = $this->gatewayFactory->createNew();
+ $gatewayConfig->setGatewayName('online_disabled');
+ $gatewayConfig->setFactoryName('online-disabled');
+ $gatewayConfig->setConfig(self::$gatewayConfigData);
+
+ $this->gatewayConfigRepository->add($gatewayConfig);
+ self::assertSame(self::$gatewayConfigData, $gatewayConfig->getConfig());
+
+ $this->entityManager->clear();
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('online_disabled');
+ self::assertSame($gatewayConfig->getConfig(), $gatewayConfigFromDatabase);
+
+ $gatewayFromRepository = $this->gatewayConfigRepository->findOneBy(['gatewayName' => 'online_disabled']);
+ self::assertSame($gatewayConfig->getConfig(), $gatewayFromRepository->getConfig());
+ self::assertSame(self::$gatewayConfigData, $gatewayConfig->getConfig());
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('online_disabled');
+ self::assertSame($gatewayConfig->getConfig(), $gatewayConfigFromDatabase);
+ self::assertSame(self::$gatewayConfigData, $gatewayConfigFromDatabase);
+ }
+
+ /** @test */
+ public function it_does_not_encrypt_empty_config(): void
+ {
+ $gatewayConfig = $this->gatewayFactory->createNew();
+ $gatewayConfig->setGatewayName('Online');
+ $gatewayConfig->setFactoryName('online');
+ $gatewayConfig->setConfig([]);
+
+ $this->gatewayConfigRepository->add($gatewayConfig);
+ self::assertSame([], $gatewayConfig->getConfig());
+
+ $this->entityManager->clear();
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('Online');
+ self::assertSame([], $gatewayConfigFromDatabase);
+
+ $gatewayFromRepository = $this->gatewayConfigRepository->findOneBy(['gatewayName' => 'Online']);
+ self::assertSame($gatewayConfig->getConfig(), $gatewayFromRepository->getConfig());
+ self::assertSame([], $gatewayConfig->getConfig());
+
+ $gatewayConfigFromDatabase = $this->getDatabaseConfigDataForGateway('Online');
+ self::assertSame($gatewayConfig->getConfig(), $gatewayConfigFromDatabase);
+ self::assertSame([], $gatewayConfigFromDatabase);
+ }
+
+ private function getDatabaseConfigDataForGateway(string $gatewayName): array
+ {
+ $result = $this->entityManager->getConnection()->executeQuery(
+ 'SELECT config FROM sylius_gateway_config WHERE gateway_name = :gatewayName',
+ ['gatewayName' => $gatewayName],
+ );
+
+ return json_decode($result->fetchOne(), true);
+ }
+
+ private function loadFixtures(array $fixtureFiles): void
+ {
+ /** @var LoaderInterface $fixtureLoader */
+ $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine');
+
+ $fixtureLoader->load($fixtureFiles, [], [], PurgeMode::createDeleteMode());
+ }
+}
diff --git a/tests/Functional/Encryption/PaymentRequestEncryptionTest.php b/tests/Functional/Encryption/PaymentRequestEncryptionTest.php
new file mode 100644
index 00000000000..738ad1f024a
--- /dev/null
+++ b/tests/Functional/Encryption/PaymentRequestEncryptionTest.php
@@ -0,0 +1,195 @@
+ */
+ private PaymentRequestRepositoryInterface $paymentRequestRepository;
+
+ /** @var PaymentRequestFactoryInterface */
+ private PaymentRequestFactoryInterface $paymentRequestFactory;
+
+ private array $fixtures;
+
+ public function setUp(): void
+ {
+ $this->entityManager = self::getContainer()->get('doctrine.orm.default_entity_manager');
+ $this->paymentRequestRepository = self::getContainer()->get('sylius.repository.payment_request');
+ $this->paymentRequestFactory = self::getContainer()->get('sylius.factory.payment_request');
+
+ $this->fixtures = $this->loadFixtures([
+ __DIR__ . '/../../DataFixtures/ORM/resources/channels.yml',
+ __DIR__ . '/../../DataFixtures/ORM/resources/payment_methods.yml',
+ ]);
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider getPayload
+ */
+ public function it_covers_encryption_and_decryption_when_saving_and_loading_the_payload(mixed $payload): void
+ {
+ $paymentRequest = $this->createTestPaymentRequest();
+ $paymentRequest->setPayload($payload);
+
+ $this->paymentRequestRepository->add($paymentRequest);
+ $hash = $paymentRequest->getHash();
+ self::assertEquals($payload, $paymentRequest->getPayload());
+
+ $this->entityManager->clear();
+
+ $payloadFromDatabase = unserialize($this->getDatabaseColumnDataForHash('payload'));
+ self::assertNotEquals($paymentRequest->getPayload(), $payloadFromDatabase);
+
+ $paymentRequestFromRepository = $this->paymentRequestRepository->find($hash);
+ self::assertEquals($paymentRequest->getPayload(), $paymentRequestFromRepository->getPayload());
+ self::assertEquals($payload, $paymentRequest->getPayload());
+
+ $payloadFromDatabase = unserialize($this->getDatabaseColumnDataForHash('payload'));
+ self::assertNotEquals($paymentRequest->getPayload(), $payloadFromDatabase);
+ self::assertNotEquals($payload, $payloadFromDatabase);
+ }
+
+ /** @test */
+ public function it_does_not_encrypt_and_decrypt_null_payloads(): void
+ {
+ $paymentRequest = $this->createTestPaymentRequest();
+ $paymentRequest->setPayload(null);
+
+ $this->paymentRequestRepository->add($paymentRequest);
+ $hash = $paymentRequest->getHash();
+ self::assertNull($paymentRequest->getPayload());
+
+ $this->entityManager->clear();
+
+ $payloadFromDatabase = unserialize($this->getDatabaseColumnDataForHash('payload'));
+ self::assertSame($paymentRequest->getPayload(), $payloadFromDatabase);
+
+ $paymentRequestFromRepository = $this->paymentRequestRepository->find($hash);
+ self::assertSame($paymentRequest->getPayload(), $paymentRequestFromRepository->getPayload());
+ self::assertNull($paymentRequest->getPayload());
+
+ $payloadFromDatabase = unserialize($this->getDatabaseColumnDataForHash('payload'));
+ self::assertEquals($paymentRequest->getPayload(), $payloadFromDatabase);
+ self::assertNull($payloadFromDatabase);
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider getResponseData
+ */
+ public function it_covers_encryption_and_decryption_when_saving_and_loading_the_response_data(
+ array $responseData,
+ ): void {
+ $paymentRequest = $this->createTestPaymentRequest();
+ $paymentRequest->setResponseData($responseData);
+
+ $this->paymentRequestRepository->add($paymentRequest);
+ $hash = $paymentRequest->getHash();
+ self::assertEquals($responseData, $paymentRequest->getResponseData());
+
+ $this->entityManager->clear();
+
+ $responseDataFromDatabase = json_decode($this->getDatabaseColumnDataForHash('response_data'), true);
+ self::assertNotEquals($paymentRequest->getResponseData(), $responseDataFromDatabase);
+
+ $paymentRequestFromRepository = $this->paymentRequestRepository->find($hash);
+ self::assertEquals($paymentRequest->getResponseData(), $paymentRequestFromRepository->getResponseData());
+ self::assertEquals($responseData, $paymentRequest->getResponseData());
+
+ $responseDataFromDatabase = json_decode($this->getDatabaseColumnDataForHash('response_data'), true);
+ self::assertNotEquals($paymentRequest->getResponseData(), $responseDataFromDatabase);
+ self::assertNotEquals($responseData, $responseDataFromDatabase);
+ }
+
+ public static function getPayload(): iterable
+ {
+ yield 'integer payload' => [42];
+ yield 'string payload' => ['payload'];
+ yield 'array payload' => [['key' => 'value']];
+ yield 'object payload' => [new \stdClass()];
+ }
+
+ public static function getResponseData(): iterable
+ {
+ yield 'integer response data' => [[42]];
+ yield 'string response data' => [['response_data']];
+ yield 'array response data' => [['key' => 'value']];
+ yield 'object response data' => [[new \stdClass()]];
+ yield 'complex response data' => [[
+ 'some_other_data' => 'data',
+ 'some_object' => new \stdClass(),
+ 'http_request' => [
+ 'query' => '?token=123',
+ 'request' => 'smth',
+ 'method' => 'GET',
+ 'uri' => 'http://example.com',
+ 'client_ip' => '127.0.0.1',
+ 'user_agent' => 'Mozilla/5.0',
+ 'content' => 'content',
+ 'headers' => ['Content-Type' => 'application/json'],
+ ],
+ ]];
+ }
+
+ private function createTestPaymentRequest(): PaymentRequestInterface
+ {
+ $order = new Order();
+ $order->setCurrencyCode('USD');
+ $order->setLocaleCode('en_US');
+ $this->entityManager->persist($order);
+ $this->entityManager->flush();
+
+ $payment = new Payment();
+ $payment->setCurrencyCode('USD');
+ $payment->setOrder($order);
+ $payment->setMethod($this->fixtures['cash_on_delivery']);
+ $this->entityManager->persist($payment);
+ $this->entityManager->flush();
+
+ return $this->paymentRequestFactory->create($payment, $payment->getMethod());
+ }
+
+ private function getDatabaseColumnDataForHash(string $column): mixed
+ {
+ $result = $this->entityManager->getConnection()->executeQuery(
+ 'SELECT * FROM sylius_payment_request LIMIT 1',
+ );
+
+ return $result->fetchAssociative()[$column];
+ }
+
+ private function loadFixtures(array $fixtureFiles): array
+ {
+ /** @var LoaderInterface $fixtureLoader */
+ $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine');
+
+ return $fixtureLoader->load($fixtureFiles, [], [], PurgeMode::createDeleteMode());
+ }
+}