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 &$mapping, WriteCommand $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 &$mapping, WriteCommand $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 $mapping, PreWriteValidationEvent $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 $inserts, PreWriteValidationEvent $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 $duplicates, PreWriteValidationEvent $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 $deletions, PreWriteValidationEvent $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 $updates, PreWriteValidationEvent $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 $violations, PreWriteValidationEvent $event): void
  331. {
  332. $event->getExceptions()->add(new WriteConstraintViolationException($violations));
  333. }
  334. }