platform/src/Core/System/SalesChannel/Validation/SalesChannelValidator.php line 49

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