src/Core/System/Language/LanguageValidator.php line 69

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