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

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