src/Core/Content/Rule/RuleValidator.php line 59

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Rule;
  3. use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionCollection;
  4. use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionDefinition;
  5. use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionEntity;
  6. use Shopware\Core\Framework\App\Aggregate\AppScriptCondition\AppScriptConditionEntity;
  7. use Shopware\Core\Framework\Context;
  8. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Exception\UnsupportedCommandTypeException;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteException;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Rule\Collector\RuleConditionRegistry;
  19. use Shopware\Core\Framework\Rule\Exception\InvalidConditionException;
  20. use Shopware\Core\Framework\Rule\ScriptRule;
  21. use Shopware\Core\Framework\Uuid\Uuid;
  22. use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
  23. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  24. use Symfony\Component\Validator\Constraint;
  25. use Symfony\Component\Validator\ConstraintViolation;
  26. use Symfony\Component\Validator\ConstraintViolationInterface;
  27. use Symfony\Component\Validator\ConstraintViolationList;
  28. use Symfony\Component\Validator\Validator\ValidatorInterface;
  29. /**
  30.  * @internal
  31.  */
  32. #[Package('business-ops')]
  33. class RuleValidator implements EventSubscriberInterface
  34. {
  35.     /**
  36.      * @internal
  37.      */
  38.     public function __construct(
  39.         private readonly ValidatorInterface $validator,
  40.         private readonly RuleConditionRegistry $ruleConditionRegistry,
  41.         private readonly EntityRepository $ruleConditionRepository,
  42.         private readonly EntityRepository $appScriptConditionRepository
  43.     ) {
  44.     }
  45.     public static function getSubscribedEvents(): array
  46.     {
  47.         return [
  48.             PreWriteValidationEvent::class => 'preValidate',
  49.         ];
  50.     }
  51.     /**
  52.      * @throws UnsupportedCommandTypeException
  53.      */
  54.     public function preValidate(PreWriteValidationEvent $event): void
  55.     {
  56.         $writeException $event->getExceptions();
  57.         $commands $event->getCommands();
  58.         $updateQueue = [];
  59.         foreach ($commands as $command) {
  60.             if ($command->getDefinition()->getClass() !== RuleConditionDefinition::class) {
  61.                 continue;
  62.             }
  63.             if ($command instanceof DeleteCommand) {
  64.                 continue;
  65.             }
  66.             if ($command instanceof InsertCommand) {
  67.                 $this->validateCondition(null$command$writeException$event->getContext());
  68.                 continue;
  69.             }
  70.             if ($command instanceof UpdateCommand) {
  71.                 $updateQueue[] = $command;
  72.                 continue;
  73.             }
  74.             throw new UnsupportedCommandTypeException($command);
  75.         }
  76.         if (!empty($updateQueue)) {
  77.             $this->validateUpdateCommands($updateQueue$writeException$event->getContext());
  78.         }
  79.     }
  80.     private function validateCondition(
  81.         ?RuleConditionEntity $condition,
  82.         WriteCommand $command,
  83.         WriteException $writeException,
  84.         Context $context
  85.     ): void {
  86.         $payload $command->getPayload();
  87.         $violationList = new ConstraintViolationList();
  88.         $type $this->getConditionType($condition$payload);
  89.         if ($type === null) {
  90.             return;
  91.         }
  92.         try {
  93.             $ruleInstance $this->ruleConditionRegistry->getRuleInstance($type);
  94.         } catch (InvalidConditionException) {
  95.             $violation $this->buildViolation(
  96.                 'This {{ value }} is not a valid condition type.',
  97.                 ['{{ value }}' => $type],
  98.                 '/type',
  99.                 'CONTENT__INVALID_RULE_TYPE_EXCEPTION'
  100.             );
  101.             $violationList->add($violation);
  102.             $writeException->add(new WriteConstraintViolationException($violationList$command->getPath()));
  103.             return;
  104.         }
  105.         $value $this->getConditionValue($condition$payload);
  106.         $ruleInstance->assign($value);
  107.         if ($ruleInstance instanceof ScriptRule) {
  108.             $this->setScriptConstraints($ruleInstance$condition$payload$context);
  109.         }
  110.         $this->validateConsistence(
  111.             $ruleInstance->getConstraints(),
  112.             $value,
  113.             $violationList
  114.         );
  115.         if ($violationList->count() > 0) {
  116.             $writeException->add(new WriteConstraintViolationException($violationList$command->getPath()));
  117.         }
  118.     }
  119.     /**
  120.      * @param array<mixed> $payload
  121.      */
  122.     private function getConditionType(?RuleConditionEntity $condition, array $payload): ?string
  123.     {
  124.         $type $condition !== null $condition->getType() : null;
  125.         if (\array_key_exists('type'$payload)) {
  126.             $type $payload['type'];
  127.         }
  128.         return $type;
  129.     }
  130.     /**
  131.      * @param array<mixed> $payload
  132.      *
  133.      * @return array<mixed>
  134.      */
  135.     private function getConditionValue(?RuleConditionEntity $condition, array $payload): array
  136.     {
  137.         $value $condition !== null $condition->getValue() : [];
  138.         if (isset($payload['value']) && $payload['value'] !== null) {
  139.             $value json_decode((string) $payload['value'], true512\JSON_THROW_ON_ERROR);
  140.         }
  141.         return $value ?? [];
  142.     }
  143.     /**
  144.      * @param array<string, array<Constraint>> $fieldValidations
  145.      * @param array<mixed> $payload
  146.      */
  147.     private function validateConsistence(array $fieldValidations, array $payloadConstraintViolationList $violationList): void
  148.     {
  149.         foreach ($fieldValidations as $fieldName => $validations) {
  150.             $violationList->addAll(
  151.                 $this->validator->startContext()
  152.                     ->atPath('/value/' $fieldName)
  153.                     ->validate($payload[$fieldName] ?? null$validations)
  154.                     ->getViolations()
  155.             );
  156.         }
  157.         foreach ($payload as $fieldName => $_value) {
  158.             if (!\array_key_exists($fieldName$fieldValidations) && $fieldName !== '_name') {
  159.                 $violationList->add(
  160.                     $this->buildViolation(
  161.                         'The property "{{ fieldName }}" is not allowed.',
  162.                         ['{{ fieldName }}' => $fieldName],
  163.                         '/value/' $fieldName
  164.                     )
  165.                 );
  166.             }
  167.         }
  168.     }
  169.     /**
  170.      * @param array<UpdateCommand> $commandQueue
  171.      */
  172.     private function validateUpdateCommands(
  173.         array $commandQueue,
  174.         WriteException $writeException,
  175.         Context $context
  176.     ): void {
  177.         $conditions $this->getSavedConditions($commandQueue$context);
  178.         foreach ($commandQueue as $command) {
  179.             $id Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
  180.             $condition $conditions->get($id);
  181.             $this->validateCondition($condition$command$writeException$context);
  182.         }
  183.     }
  184.     /**
  185.      * @param array<UpdateCommand> $commandQueue
  186.      */
  187.     private function getSavedConditions(array $commandQueueContext $context): RuleConditionCollection
  188.     {
  189.         $ids array_map(function ($command) {
  190.             $uuidBytes $command->getPrimaryKey()['id'];
  191.             return Uuid::fromBytesToHex($uuidBytes);
  192.         }, $commandQueue);
  193.         $criteria = new Criteria($ids);
  194.         $criteria->setLimit(null);
  195.         /** @var RuleConditionCollection $entities */
  196.         $entities $this->ruleConditionRepository->search($criteria$context)->getEntities();
  197.         return $entities;
  198.     }
  199.     /**
  200.      * @param array<int|string> $parameters
  201.      */
  202.     private function buildViolation(
  203.         string $messageTemplate,
  204.         array $parameters,
  205.         ?string $propertyPath null,
  206.         ?string $code null
  207.     ): ConstraintViolationInterface {
  208.         return new ConstraintViolation(
  209.             str_replace(array_keys($parameters), array_values($parameters), $messageTemplate),
  210.             $messageTemplate,
  211.             $parameters,
  212.             null,
  213.             $propertyPath,
  214.             null,
  215.             null,
  216.             $code
  217.         );
  218.     }
  219.     /**
  220.      * @param array<mixed> $payload
  221.      */
  222.     private function setScriptConstraints(
  223.         ScriptRule $ruleInstance,
  224.         ?RuleConditionEntity $condition,
  225.         array $payload,
  226.         Context $context
  227.     ): void {
  228.         $script null;
  229.         if (isset($payload['script_id'])) {
  230.             $scriptId Uuid::fromBytesToHex($payload['script_id']);
  231.             $script $this->appScriptConditionRepository->search(new Criteria([$scriptId]), $context)->get($scriptId);
  232.         } elseif ($condition && $condition->getAppScriptCondition()) {
  233.             $script $condition->getAppScriptCondition();
  234.         }
  235.         if (!$script instanceof AppScriptConditionEntity || !\is_array($script->getConstraints())) {
  236.             return;
  237.         }
  238.         $ruleInstance->setConstraints($script->getConstraints());
  239.     }
  240. }