<?php declare(strict_types=1);namespace Shopware\Core\System\Language;use Doctrine\DBAL\Connection;use Doctrine\DBAL\FetchMode;use Shopware\Core\Defaults;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\CascadeDeleteCommand;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;use Shopware\Core\Framework\Log\Package;use Shopware\Core\Framework\Uuid\Uuid;use Shopware\Core\Framework\Validation\WriteConstraintViolationException;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\Validator\ConstraintViolation;use Symfony\Component\Validator\ConstraintViolationInterface;use Symfony\Component\Validator\ConstraintViolationList;/** * @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0 */#[Package('core')]class LanguageValidator implements EventSubscriberInterface{ public const VIOLATION_PARENT_HAS_PARENT = 'parent_has_parent_violation'; public const VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE = 'code_required_for_root_language'; public const VIOLATION_DELETE_DEFAULT_LANGUAGE = 'delete_default_language_violation'; public const VIOLATION_DEFAULT_LANGUAGE_PARENT = 'default_language_parent_violation'; /** * @deprecated tag:v6.5.0 - const will be removed in v6.5.0 */ public const DEFAULT_LANGUAGES = [Defaults::LANGUAGE_SYSTEM]; private Connection $connection; /** * @internal */ public function __construct(Connection $connection) { $this->connection = $connection; } public static function getSubscribedEvents(): array { return [ PreWriteValidationEvent::class => 'preValidate', PostWriteValidationEvent::class => 'postValidate', ]; } public function postValidate(PostWriteValidationEvent $event): void { $commands = $event->getCommands(); $affectedIds = $this->getAffectedIds($commands); if (\count($affectedIds) === 0) { return; } $violations = new ConstraintViolationList(); $violations->addAll($this->getInheritanceViolations($affectedIds)); $violations->addAll($this->getMissingTranslationCodeViolations($affectedIds)); if ($violations->count() > 0) { $event->getExceptions()->add(new WriteConstraintViolationException($violations)); } } public function preValidate(PreWriteValidationEvent $event): void { $commands = $event->getCommands(); foreach ($commands as $command) { $violations = new ConstraintViolationList(); if ($command instanceof CascadeDeleteCommand || $command->getDefinition()->getClass() !== LanguageDefinition::class) { continue; } $pk = $command->getPrimaryKey(); $id = mb_strtolower(Uuid::fromBytesToHex($pk['id'])); if ($command instanceof DeleteCommand && $id === Defaults::LANGUAGE_SYSTEM) { $violations->add( $this->buildViolation( 'The default language {{ id }} cannot be deleted.', ['{{ id }}' => $id], '/' . $id, $id, self::VIOLATION_DELETE_DEFAULT_LANGUAGE ) ); } if ($command instanceof UpdateCommand && $id === Defaults::LANGUAGE_SYSTEM) { $payload = $command->getPayload(); if (\array_key_exists('parent_id', $payload) && $payload['parent_id'] !== null) { $violations->add( $this->buildViolation( 'The default language {{ id }} cannot inherit from another language.', ['{{ id }}' => $id], '/parentId', $payload['parent_id'], self::VIOLATION_DEFAULT_LANGUAGE_PARENT ) ); } } if ($violations->count() > 0) { $event->getExceptions()->add(new WriteConstraintViolationException($violations, $command->getPath())); } } } /** * @param array<string> $affectedIds */ private function getInheritanceViolations(array $affectedIds): ConstraintViolationList { $statement = $this->connection->executeQuery( 'SELECT child.id FROM language child INNER JOIN language parent ON parent.id = child.parent_id WHERE (child.id IN (:ids) OR child.parent_id IN (:ids)) AND parent.parent_id IS NOT NULL', ['ids' => $affectedIds], ['ids' => Connection::PARAM_STR_ARRAY] ); $ids = $statement->fetchAll(FetchMode::COLUMN); $violations = new ConstraintViolationList(); foreach ($ids as $binId) { $id = Uuid::fromBytesToHex($binId); $violations->add( $this->buildViolation( 'Language inheritance limit for the child {{ id }} exceeded. A Language must not be nested deeper than one level.', ['{{ id }}' => $id], '/' . $id . '/parentId', $id, self::VIOLATION_PARENT_HAS_PARENT ) ); } return $violations; } /** * @param array<string> $affectedIds */ private function getMissingTranslationCodeViolations(array $affectedIds): ConstraintViolationList { $statement = $this->connection->executeQuery( 'SELECT lang.id FROM language lang LEFT JOIN locale l ON lang.translation_code_id = l.id WHERE l.id IS NULL # no translation code AND lang.parent_id IS NULL # root AND lang.id IN (:ids)', ['ids' => $affectedIds], ['ids' => Connection::PARAM_STR_ARRAY] ); $ids = $statement->fetchAll(FetchMode::COLUMN); $violations = new ConstraintViolationList(); foreach ($ids as $binId) { $id = Uuid::fromBytesToHex($binId); $violations->add( $this->buildViolation( 'Root language {{ id }} requires a translation code', ['{{ id }}' => $id], '/' . $id . '/translationCodeId', $id, self::VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE ) ); } return $violations; } /** * @param WriteCommand[] $commands * * @return array<string> */ private function getAffectedIds(array $commands): array { $ids = []; foreach ($commands as $command) { if ($command->getDefinition()->getClass() !== LanguageDefinition::class) { continue; } if ($command instanceof InsertCommand || $command instanceof UpdateCommand) { $ids[] = $command->getPrimaryKey()['id']; } } return $ids; } /** * @param array<string, string> $parameters */ private function buildViolation( string $messageTemplate, array $parameters, ?string $propertyPath = null, ?string $invalidValue = null, ?string $code = null ): ConstraintViolationInterface { return new ConstraintViolation( str_replace(array_keys($parameters), array_values($parameters), $messageTemplate), $messageTemplate, $parameters, null, $propertyPath, $invalidValue, null, $code ); }}