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

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