vendor/shopware/core/System/Language/LanguageValidator.php line 77

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\Language;
  3. use Doctrine\DBAL\Connection;
  4. use Doctrine\DBAL\FetchMode;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\CascadeDeleteCommand;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  13. use Shopware\Core\Framework\Log\Package;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. use Symfony\Component\Validator\ConstraintViolation;
  18. use Symfony\Component\Validator\ConstraintViolationInterface;
  19. use Symfony\Component\Validator\ConstraintViolationList;
  20. /**
  21. * @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0
  22. */
  23. #[Package('core')]
  24. class LanguageValidator implements EventSubscriberInterface
  25. {
  26. public const VIOLATION_PARENT_HAS_PARENT = 'parent_has_parent_violation';
  27. public const VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE = 'code_required_for_root_language';
  28. public const VIOLATION_DELETE_DEFAULT_LANGUAGE = 'delete_default_language_violation';
  29. public const VIOLATION_DEFAULT_LANGUAGE_PARENT = 'default_language_parent_violation';
  30. /**
  31. * @deprecated tag:v6.5.0 - const will be removed in v6.5.0
  32. */
  33. public const DEFAULT_LANGUAGES = [Defaults::LANGUAGE_SYSTEM];
  34. private Connection $connection;
  35. /**
  36. * @internal
  37. */
  38. public function __construct(Connection $connection)
  39. {
  40. $this->connection = $connection;
  41. }
  42. public static function getSubscribedEvents(): array
  43. {
  44. return [
  45. PreWriteValidationEvent::class => 'preValidate',
  46. PostWriteValidationEvent::class => 'postValidate',
  47. ];
  48. }
  49. public function postValidate(PostWriteValidationEvent $event): void
  50. {
  51. $commands = $event->getCommands();
  52. $affectedIds = $this->getAffectedIds($commands);
  53. if (\count($affectedIds) === 0) {
  54. return;
  55. }
  56. $violations = new ConstraintViolationList();
  57. $violations->addAll($this->getInheritanceViolations($affectedIds));
  58. $violations->addAll($this->getMissingTranslationCodeViolations($affectedIds));
  59. if ($violations->count() > 0) {
  60. $event->getExceptions()->add(new WriteConstraintViolationException($violations));
  61. }
  62. }
  63. public function preValidate(PreWriteValidationEvent $event): void
  64. {
  65. $commands = $event->getCommands();
  66. foreach ($commands as $command) {
  67. $violations = new ConstraintViolationList();
  68. if ($command instanceof CascadeDeleteCommand || $command->getDefinition()->getClass() !== LanguageDefinition::class) {
  69. continue;
  70. }
  71. $pk = $command->getPrimaryKey();
  72. $id = mb_strtolower(Uuid::fromBytesToHex($pk['id']));
  73. if ($command instanceof DeleteCommand && $id === Defaults::LANGUAGE_SYSTEM) {
  74. $violations->add(
  75. $this->buildViolation(
  76. 'The default language {{ id }} cannot be deleted.',
  77. ['{{ id }}' => $id],
  78. '/' . $id,
  79. $id,
  80. self::VIOLATION_DELETE_DEFAULT_LANGUAGE
  81. )
  82. );
  83. }
  84. if ($command instanceof UpdateCommand && $id === Defaults::LANGUAGE_SYSTEM) {
  85. $payload = $command->getPayload();
  86. if (\array_key_exists('parent_id', $payload) && $payload['parent_id'] !== null) {
  87. $violations->add(
  88. $this->buildViolation(
  89. 'The default language {{ id }} cannot inherit from another language.',
  90. ['{{ id }}' => $id],
  91. '/parentId',
  92. $payload['parent_id'],
  93. self::VIOLATION_DEFAULT_LANGUAGE_PARENT
  94. )
  95. );
  96. }
  97. }
  98. if ($violations->count() > 0) {
  99. $event->getExceptions()->add(new WriteConstraintViolationException($violations, $command->getPath()));
  100. }
  101. }
  102. }
  103. /**
  104. * @param array<string> $affectedIds
  105. */
  106. private function getInheritanceViolations(array $affectedIds): ConstraintViolationList
  107. {
  108. $statement = $this->connection->executeQuery(
  109. 'SELECT child.id
  110. FROM language child
  111. INNER JOIN language parent ON parent.id = child.parent_id
  112. WHERE (child.id IN (:ids) OR child.parent_id IN (:ids))
  113. AND parent.parent_id IS NOT NULL',
  114. ['ids' => $affectedIds],
  115. ['ids' => Connection::PARAM_STR_ARRAY]
  116. );
  117. $ids = $statement->fetchAll(FetchMode::COLUMN);
  118. $violations = new ConstraintViolationList();
  119. foreach ($ids as $binId) {
  120. $id = Uuid::fromBytesToHex($binId);
  121. $violations->add(
  122. $this->buildViolation(
  123. 'Language inheritance limit for the child {{ id }} exceeded. A Language must not be nested deeper than one level.',
  124. ['{{ id }}' => $id],
  125. '/' . $id . '/parentId',
  126. $id,
  127. self::VIOLATION_PARENT_HAS_PARENT
  128. )
  129. );
  130. }
  131. return $violations;
  132. }
  133. /**
  134. * @param array<string> $affectedIds
  135. */
  136. private function getMissingTranslationCodeViolations(array $affectedIds): ConstraintViolationList
  137. {
  138. $statement = $this->connection->executeQuery(
  139. 'SELECT lang.id
  140. FROM language lang
  141. LEFT JOIN locale l ON lang.translation_code_id = l.id
  142. WHERE l.id IS NULL # no translation code
  143. AND lang.parent_id IS NULL # root
  144. AND lang.id IN (:ids)',
  145. ['ids' => $affectedIds],
  146. ['ids' => Connection::PARAM_STR_ARRAY]
  147. );
  148. $ids = $statement->fetchAll(FetchMode::COLUMN);
  149. $violations = new ConstraintViolationList();
  150. foreach ($ids as $binId) {
  151. $id = Uuid::fromBytesToHex($binId);
  152. $violations->add(
  153. $this->buildViolation(
  154. 'Root language {{ id }} requires a translation code',
  155. ['{{ id }}' => $id],
  156. '/' . $id . '/translationCodeId',
  157. $id,
  158. self::VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE
  159. )
  160. );
  161. }
  162. return $violations;
  163. }
  164. /**
  165. * @param WriteCommand[] $commands
  166. *
  167. * @return array<string>
  168. */
  169. private function getAffectedIds(array $commands): array
  170. {
  171. $ids = [];
  172. foreach ($commands as $command) {
  173. if ($command->getDefinition()->getClass() !== LanguageDefinition::class) {
  174. continue;
  175. }
  176. if ($command instanceof InsertCommand || $command instanceof UpdateCommand) {
  177. $ids[] = $command->getPrimaryKey()['id'];
  178. }
  179. }
  180. return $ids;
  181. }
  182. /**
  183. * @param array<string, string> $parameters
  184. */
  185. private function buildViolation(
  186. string $messageTemplate,
  187. array $parameters,
  188. ?string $propertyPath = null,
  189. ?string $invalidValue = null,
  190. ?string $code = null
  191. ): ConstraintViolationInterface {
  192. return new ConstraintViolation(
  193. str_replace(array_keys($parameters), array_values($parameters), $messageTemplate),
  194. $messageTemplate,
  195. $parameters,
  196. null,
  197. $propertyPath,
  198. $invalidValue,
  199. null,
  200. $code
  201. );
  202. }
  203. }