vendor/shopware/core/System/SalesChannel/Validation/SalesChannelValidator.php line 52

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\SalesChannel\Validation;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Defaults;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  10. use Shopware\Core\Framework\Uuid\Uuid;
  11. use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
  12. use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelLanguage\SalesChannelLanguageDefinition;
  13. use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
  14. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  15. use Symfony\Component\Validator\ConstraintViolation;
  16. use Symfony\Component\Validator\ConstraintViolationList;
  17. class SalesChannelValidator implements EventSubscriberInterface
  18. {
  19.     private const INSERT_VALIDATION_MESSAGE 'The sales channel with id "%s" does not have a default sales channel language id in the language list.';
  20.     private const INSERT_VALIDATION_CODE 'SYSTEM__NO_GIVEN_DEFAULT_LANGUAGE_ID';
  21.     private const DUPLICATED_ENTRY_VALIDATION_MESSAGE 'The sales channel language "%s" for the sales channel "%s" already exists.';
  22.     private const DUPLICATED_ENTRY_VALIDATION_CODE 'SYSTEM__DUPLICATED_SALES_CHANNEL_LANGUAGE';
  23.     private const UPDATE_VALIDATION_MESSAGE 'Cannot update default language id because the given id is not in the language list of sales channel with id "%s"';
  24.     private const UPDATE_VALIDATION_CODE 'SYSTEM__CANNOT_UPDATE_DEFAULT_LANGUAGE_ID';
  25.     private const DELETE_VALIDATION_MESSAGE 'Cannot delete default language id from language list of the sales channel with id "%s".';
  26.     private const DELETE_VALIDATION_CODE 'SYSTEM__CANNOT_DELETE_DEFAULT_LANGUAGE_ID';
  27.     private Connection $connection;
  28.     /**
  29.      * @internal
  30.      */
  31.     public function __construct(
  32.         Connection $connection
  33.     ) {
  34.         $this->connection $connection;
  35.     }
  36.     public static function getSubscribedEvents(): array
  37.     {
  38.         return [
  39.             PreWriteValidationEvent::class => 'handleSalesChannelLanguageIds',
  40.         ];
  41.     }
  42.     public function handleSalesChannelLanguageIds(PreWriteValidationEvent $event): void
  43.     {
  44.         $mapping $this->extractMapping($event);
  45.         if (!$mapping) {
  46.             return;
  47.         }
  48.         $salesChannelIds array_keys($mapping);
  49.         $states $this->fetchCurrentLanguageStates($salesChannelIds);
  50.         $mapping $this->mergeCurrentStatesWithMapping($mapping$states);
  51.         $this->validateLanguages($mapping$event);
  52.     }
  53.     /**
  54.      * Build a key map with the following data structure:
  55.      *
  56.      * 'sales_channel_id' => [
  57.      *     'current_default' => 'en',
  58.      *     'new_default' => 'de',
  59.      *     'inserts' => ['de', 'en'],
  60.      *     'updates' => ['de', 'de'],
  61.      *     'deletions' => ['gb'],
  62.      *     'state' => ['en', 'gb']
  63.      * ]
  64.      */
  65.     private function extractMapping(PreWriteValidationEvent $event): array
  66.     {
  67.         $mapping = [];
  68.         foreach ($event->getCommands() as $command) {
  69.             if ($command->getDefinition() instanceof SalesChannelDefinition) {
  70.                 $this->handleSalesChannelMapping($mapping$command);
  71.                 continue;
  72.             }
  73.             if ($command->getDefinition() instanceof SalesChannelLanguageDefinition) {
  74.                 $this->handleSalesChannelLanguageMapping($mapping$command);
  75.             }
  76.         }
  77.         return $mapping;
  78.     }
  79.     private function handleSalesChannelMapping(array &$mappingWriteCommand $command): void
  80.     {
  81.         if (!isset($command->getPayload()['language_id'])) {
  82.             return;
  83.         }
  84.         if ($command instanceof UpdateCommand) {
  85.             $id Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
  86.             $mapping[$id]['updates'] = Uuid::fromBytesToHex($command->getPayload()['language_id']);
  87.             return;
  88.         }
  89.         if (!$command instanceof InsertCommand || !$this->isSupportedSalesChannelType($command)) {
  90.             return;
  91.         }
  92.         $id Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
  93.         $mapping[$id]['new_default'] = Uuid::fromBytesToHex($command->getPayload()['language_id']);
  94.         $mapping[$id]['inserts'] = [];
  95.         $mapping[$id]['state'] = [];
  96.     }
  97.     private function isSupportedSalesChannelType(WriteCommand $command): bool
  98.     {
  99.         $typeId Uuid::fromBytesToHex($command->getPayload()['type_id']);
  100.         return $typeId === Defaults::SALES_CHANNEL_TYPE_STOREFRONT
  101.             || $typeId === Defaults::SALES_CHANNEL_TYPE_API;
  102.     }
  103.     private function handleSalesChannelLanguageMapping(array &$mappingWriteCommand $command): void
  104.     {
  105.         $language Uuid::fromBytesToHex($command->getPrimaryKey()['language_id']);
  106.         $id Uuid::fromBytesToHex($command->getPrimaryKey()['sales_channel_id']);
  107.         $mapping[$id]['state'] = [];
  108.         if ($command instanceof DeleteCommand) {
  109.             $mapping[$id]['deletions'][] = $language;
  110.             return;
  111.         }
  112.         if ($command instanceof InsertCommand) {
  113.             $mapping[$id]['inserts'][] = $language;
  114.         }
  115.     }
  116.     private function validateLanguages(array $mappingPreWriteValidationEvent $event): void
  117.     {
  118.         $inserts = [];
  119.         $duplicates = [];
  120.         $deletions = [];
  121.         $updates = [];
  122.         foreach ($mapping as $id => $channel) {
  123.             if (isset($channel['inserts'])) {
  124.                 if (!$this->validInsertCase($channel)) {
  125.                     $inserts[$id] = $channel['new_default'];
  126.                 }
  127.                 $duplicatedIds $this->getDuplicates($channel);
  128.                 if ($duplicatedIds) {
  129.                     $duplicates[$id] = $duplicatedIds;
  130.                 }
  131.             }
  132.             if (isset($channel['deletions']) && !$this->validDeleteCase($channel)) {
  133.                 $deletions[$id] = $channel['current_default'];
  134.             }
  135.             if (isset($channel['updates']) && !$this->validUpdateCase($channel)) {
  136.                 $updates[$id] = $channel['updates'];
  137.             }
  138.         }
  139.         $this->writeInsertViolationExceptions($inserts$event);
  140.         $this->writeDuplicateViolationExceptions($duplicates$event);
  141.         $this->writeDeleteViolationExceptions($deletions$event);
  142.         $this->writeUpdateViolationExceptions($updates$event);
  143.     }
  144.     private function validInsertCase(array $channel): bool
  145.     {
  146.         return empty($channel['new_default'])
  147.             || \in_array($channel['new_default'], $channel['inserts'], true);
  148.     }
  149.     private function validUpdateCase(array $channel): bool
  150.     {
  151.         $updateId $channel['updates'];
  152.         return \in_array($updateId$channel['state'], true)
  153.             || empty($channel['new_default']) && $updateId === $channel['current_default']
  154.             || isset($channel['inserts']) && \in_array($updateId$channel['inserts'], true);
  155.     }
  156.     private function validDeleteCase(array $channel): bool
  157.     {
  158.         return !\in_array($channel['current_default'], $channel['deletions'], true);
  159.     }
  160.     private function getDuplicates(array $channel): array
  161.     {
  162.         return array_intersect($channel['state'], $channel['inserts']);
  163.     }
  164.     private function writeInsertViolationExceptions(array $insertsPreWriteValidationEvent $event): void
  165.     {
  166.         if (!$inserts) {
  167.             return;
  168.         }
  169.         $violations = new ConstraintViolationList();
  170.         $salesChannelIds array_keys($inserts);
  171.         foreach ($salesChannelIds as $id) {
  172.             $violations->add(new ConstraintViolation(
  173.                 sprintf(self::INSERT_VALIDATION_MESSAGE$id),
  174.                 sprintf(self::INSERT_VALIDATION_MESSAGE'{{ salesChannelId }}'),
  175.                 ['{{ salesChannelId }}' => $id],
  176.                 null,
  177.                 '/',
  178.                 null,
  179.                 null,
  180.                 self::INSERT_VALIDATION_CODE
  181.             ));
  182.         }
  183.         $this->writeViolationException($violations$event);
  184.     }
  185.     private function writeDuplicateViolationExceptions(array $duplicatesPreWriteValidationEvent $event): void
  186.     {
  187.         if (!$duplicates) {
  188.             return;
  189.         }
  190.         $violations = new ConstraintViolationList();
  191.         foreach ($duplicates as $id => $duplicateLanguages) {
  192.             foreach ($duplicateLanguages as $languageId) {
  193.                 $violations->add(new ConstraintViolation(
  194.                     sprintf(self::DUPLICATED_ENTRY_VALIDATION_MESSAGE$languageId$id),
  195.                     sprintf(self::DUPLICATED_ENTRY_VALIDATION_MESSAGE'{{ languageId }}''{{ salesChannelId }}'),
  196.                     [
  197.                         '{{ salesChannelId }}' => $id,
  198.                         '{{ languageId }}' => $languageId,
  199.                     ],
  200.                     null,
  201.                     '/',
  202.                     null,
  203.                     null,
  204.                     self::DUPLICATED_ENTRY_VALIDATION_CODE
  205.                 ));
  206.             }
  207.         }
  208.         $this->writeViolationException($violations$event);
  209.     }
  210.     private function writeDeleteViolationExceptions(array $deletionsPreWriteValidationEvent $event): void
  211.     {
  212.         if (!$deletions) {
  213.             return;
  214.         }
  215.         $violations = new ConstraintViolationList();
  216.         $salesChannelIds array_keys($deletions);
  217.         foreach ($salesChannelIds as $id) {
  218.             $violations->add(new ConstraintViolation(
  219.                 sprintf(self::DELETE_VALIDATION_MESSAGE$id),
  220.                 sprintf(self::DELETE_VALIDATION_MESSAGE'{{ salesChannelId }}'),
  221.                 ['{{ salesChannelId }}' => $id],
  222.                 null,
  223.                 '/',
  224.                 null,
  225.                 null,
  226.                 self::DELETE_VALIDATION_CODE
  227.             ));
  228.         }
  229.         $this->writeViolationException($violations$event);
  230.     }
  231.     private function writeUpdateViolationExceptions(array $updatesPreWriteValidationEvent $event): void
  232.     {
  233.         if (!$updates) {
  234.             return;
  235.         }
  236.         $violations = new ConstraintViolationList();
  237.         $salesChannelIds array_keys($updates);
  238.         foreach ($salesChannelIds as $id) {
  239.             $violations->add(new ConstraintViolation(
  240.                 sprintf(self::UPDATE_VALIDATION_MESSAGE$id),
  241.                 sprintf(self::UPDATE_VALIDATION_MESSAGE'{{ salesChannelId }}'),
  242.                 ['{{ salesChannelId }}' => $id],
  243.                 null,
  244.                 '/',
  245.                 null,
  246.                 null,
  247.                 self::UPDATE_VALIDATION_CODE
  248.             ));
  249.         }
  250.         $this->writeViolationException($violations$event);
  251.     }
  252.     /**
  253.      * @param array<string> $salesChannelIds
  254.      */
  255.     private function fetchCurrentLanguageStates(array $salesChannelIds): array
  256.     {
  257.         return $this->connection->fetchAllAssociative(
  258.             'SELECT LOWER(HEX(sales_channel.id)) AS sales_channel_id,
  259.             LOWER(HEX(sales_channel.language_id)) AS current_default,
  260.             LOWER(HEX(mapping.language_id)) AS language_id
  261.             FROM sales_channel
  262.             LEFT JOIN sales_channel_language mapping
  263.                 ON mapping.sales_channel_id = sales_channel.id
  264.                 WHERE sales_channel.id IN (:ids)',
  265.             ['ids' => Uuid::fromHexToBytesList($salesChannelIds)],
  266.             ['ids' => Connection::PARAM_STR_ARRAY]
  267.         );
  268.     }
  269.     private function mergeCurrentStatesWithMapping(array $mapping, array $states): array
  270.     {
  271.         foreach ($states as $record) {
  272.             $id $record['sales_channel_id'];
  273.             $mapping[$id]['current_default'] = $record['current_default'];
  274.             $mapping[$id]['state'][] = $record['language_id'];
  275.         }
  276.         return $mapping;
  277.     }
  278.     private function writeViolationException(ConstraintViolationList $violationsPreWriteValidationEvent $event): void
  279.     {
  280.         $event->getExceptions()->add(new WriteConstraintViolationException($violations));
  281.     }
  282. }