vendor/shopware/core/Content/Rule/RuleValidator.php line 71

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