vendor/shopware/core/Content/Rule/DataAbstractionLayer/RuleAreaUpdater.php line 95

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Rule\DataAbstractionLayer;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\CachedRuleLoader;
  5. use Shopware\Core\Content\Rule\RuleDefinition;
  6. use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
  7. use Shopware\Core\Framework\DataAbstractionLayer\CompiledFieldCollection;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  25. use Shopware\Core\Framework\Log\Package;
  26. use Shopware\Core\Framework\Rule\Collector\RuleConditionRegistry;
  27. use Shopware\Core\Framework\Uuid\Uuid;
  28. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  29. /**
  30. * @internal
  31. */
  32. #[Package('business-ops')]
  33. class RuleAreaUpdater implements EventSubscriberInterface
  34. {
  35. private Connection $connection;
  36. private RuleDefinition $definition;
  37. private RuleConditionRegistry $conditionRegistry;
  38. private CacheInvalidator $cacheInvalidator;
  39. /**
  40. * @internal
  41. */
  42. public function __construct(
  43. Connection $connection,
  44. RuleDefinition $definition,
  45. RuleConditionRegistry $conditionRegistry,
  46. CacheInvalidator $cacheInvalidator
  47. ) {
  48. $this->connection = $connection;
  49. $this->definition = $definition;
  50. $this->conditionRegistry = $conditionRegistry;
  51. $this->cacheInvalidator = $cacheInvalidator;
  52. }
  53. public static function getSubscribedEvents(): array
  54. {
  55. return [
  56. PreWriteValidationEvent::class => 'triggerChangeSet',
  57. EntityWrittenContainerEvent::class => 'onEntityWritten',
  58. ];
  59. }
  60. public function triggerChangeSet(PreWriteValidationEvent $event): void
  61. {
  62. $associatedEntities = $this->getAssociationEntities();
  63. foreach ($event->getCommands() as $command) {
  64. $definition = $command->getDefinition();
  65. $entity = $definition->getEntityName();
  66. if (!$command instanceof ChangeSetAware || !\in_array($entity, $associatedEntities, true)) {
  67. continue;
  68. }
  69. if ($command instanceof DeleteCommand) {
  70. $command->requestChangeSet();
  71. continue;
  72. }
  73. foreach ($this->getForeignKeyFields($definition) as $field) {
  74. if ($command->hasField($field->getStorageName())) {
  75. $command->requestChangeSet();
  76. }
  77. }
  78. }
  79. }
  80. public function onEntityWritten(EntityWrittenContainerEvent $event): void
  81. {
  82. $associationFields = $this->getAssociationFields();
  83. $ruleIds = [];
  84. foreach ($event->getEvents() ?? [] as $nestedEvent) {
  85. if (!$nestedEvent instanceof EntityWrittenEvent) {
  86. continue;
  87. }
  88. $definition = $this->getAssociationDefinitionByEntity($associationFields, $nestedEvent->getEntityName());
  89. if (!$definition) {
  90. continue;
  91. }
  92. $ruleIds = $this->hydrateRuleIds($this->getForeignKeyFields($definition), $nestedEvent, $ruleIds);
  93. }
  94. if (empty($ruleIds)) {
  95. return;
  96. }
  97. $this->update(Uuid::fromBytesToHexList(array_unique(array_filter($ruleIds))));
  98. $this->cacheInvalidator->invalidate([CachedRuleLoader::CACHE_KEY]);
  99. }
  100. /**
  101. * @param list<string> $ids
  102. */
  103. public function update(array $ids): void
  104. {
  105. $associationFields = $this->getAssociationFields();
  106. $areas = $this->getAreas($ids, $associationFields);
  107. $update = new RetryableQuery(
  108. $this->connection,
  109. $this->connection->prepare('UPDATE `rule` SET `areas` = :areas WHERE `id` = :id')
  110. );
  111. /** @var array<string, string[]> $associations */
  112. foreach ($areas as $id => $associations) {
  113. $areas = [];
  114. foreach ($associations as $propertyName => $match) {
  115. if ((bool) $match === false) {
  116. continue;
  117. }
  118. if ($propertyName === 'flowCondition') {
  119. $areas = array_unique(array_merge($areas, [RuleAreas::FLOW_CONDITION_AREA]));
  120. continue;
  121. }
  122. $field = $associationFields->get($propertyName);
  123. if (!$field || !$flag = $field->getFlag(RuleAreas::class)) {
  124. continue;
  125. }
  126. $areas = array_unique(array_merge($areas, $flag instanceof RuleAreas ? $flag->getAreas() : []));
  127. }
  128. $update->execute([
  129. 'areas' => json_encode(array_values($areas)),
  130. 'id' => Uuid::fromHexToBytes($id),
  131. ]);
  132. }
  133. }
  134. /**
  135. * @param FkField[] $fields
  136. * @param string[] $ruleIds
  137. *
  138. * @return string[]
  139. */
  140. private function hydrateRuleIds(array $fields, EntityWrittenEvent $nestedEvent, array $ruleIds): array
  141. {
  142. foreach ($nestedEvent->getWriteResults() as $result) {
  143. $changeSet = $result->getChangeSet();
  144. $payload = $result->getPayload();
  145. foreach ($fields as $field) {
  146. if ($changeSet && $changeSet->hasChanged($field->getStorageName())) {
  147. $ruleIds[] = $changeSet->getBefore($field->getStorageName());
  148. $ruleIds[] = $changeSet->getAfter($field->getStorageName());
  149. }
  150. if ($changeSet) {
  151. continue;
  152. }
  153. if (!empty($payload[$field->getPropertyName()])) {
  154. $ruleIds[] = Uuid::fromHexToBytes($payload[$field->getPropertyName()]);
  155. }
  156. }
  157. }
  158. return $ruleIds;
  159. }
  160. /**
  161. * @param list<string> $ids
  162. *
  163. * @return array<string, string[][]>
  164. */
  165. private function getAreas(array $ids, CompiledFieldCollection $associationFields): array
  166. {
  167. $query = new QueryBuilder($this->connection);
  168. $query->select('LOWER(HEX(`rule`.`id`)) AS array_key')
  169. ->from('rule')
  170. ->andWhere('`rule`.`id` IN (:ids)');
  171. /** @var AssociationField $associationField */
  172. foreach ($associationFields->getElements() as $associationField) {
  173. $this->addSelect($query, $associationField);
  174. }
  175. $this->addFlowConditionSelect($query);
  176. $query->setParameter(
  177. 'ids',
  178. Uuid::fromHexToBytesList($ids),
  179. Connection::PARAM_STR_ARRAY
  180. )->setParameter(
  181. 'flowTypes',
  182. $this->conditionRegistry->getFlowRuleNames(),
  183. Connection::PARAM_STR_ARRAY
  184. );
  185. return FetchModeHelper::groupUnique($query->executeQuery()->fetchAllAssociative());
  186. }
  187. private function addSelect(QueryBuilder $query, AssociationField $associationField): void
  188. {
  189. $template = 'EXISTS(%s) AS %s';
  190. $propertyName = $associationField->getPropertyName();
  191. if ($associationField instanceof OneToOneAssociationField || $associationField instanceof ManyToOneAssociationField) {
  192. $template = 'IF(%s.%s IS NOT NULL, 1, 0) AS %s';
  193. $query->addSelect(sprintf($template, '`rule`', $this->escape($associationField->getStorageName()), $propertyName));
  194. return;
  195. }
  196. if ($associationField instanceof ManyToManyAssociationField) {
  197. $mappingTable = $this->escape($associationField->getMappingDefinition()->getEntityName());
  198. $mappingLocalColumn = $this->escape($associationField->getMappingLocalColumn());
  199. $localColumn = $this->escape($associationField->getLocalField());
  200. $subQuery = (new QueryBuilder($this->connection))
  201. ->select('1')
  202. ->from($mappingTable)
  203. ->andWhere(sprintf('%s = `rule`.%s', $mappingLocalColumn, $localColumn));
  204. $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
  205. return;
  206. }
  207. if ($associationField instanceof OneToManyAssociationField) {
  208. $referenceTable = $this->escape($associationField->getReferenceDefinition()->getEntityName());
  209. $referenceColumn = $this->escape($associationField->getReferenceField());
  210. $localColumn = $this->escape($associationField->getLocalField());
  211. $subQuery = (new QueryBuilder($this->connection))
  212. ->select('1')
  213. ->from($referenceTable)
  214. ->andWhere(sprintf('%s = `rule`.%s', $referenceColumn, $localColumn));
  215. $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
  216. }
  217. }
  218. private function addFlowConditionSelect(QueryBuilder $query): void
  219. {
  220. $subQuery = (new QueryBuilder($this->connection))
  221. ->select('1')
  222. ->from('rule_condition')
  223. ->andWhere('`rule_id` = `rule`.`id`')
  224. ->andWhere('`type` IN (:flowTypes)');
  225. $query->addSelect(sprintf('EXISTS(%s) AS flowCondition', $subQuery->getSQL()));
  226. }
  227. private function escape(string $string): string
  228. {
  229. return EntityDefinitionQueryHelper::escape($string);
  230. }
  231. private function getAssociationFields(): CompiledFieldCollection
  232. {
  233. return $this->definition
  234. ->getFields()
  235. ->filterByFlag(RuleAreas::class);
  236. }
  237. /**
  238. * @return FkField[]
  239. */
  240. private function getForeignKeyFields(EntityDefinition $definition): array
  241. {
  242. /** @var FkField[] $fields */
  243. $fields = $definition->getFields()->filterInstance(FkField::class)->filter(function (FkField $fk): bool {
  244. return $fk->getReferenceDefinition()->getEntityName() === $this->definition->getEntityName();
  245. })->getElements();
  246. return $fields;
  247. }
  248. /**
  249. * @return string[]
  250. */
  251. private function getAssociationEntities(): array
  252. {
  253. return $this->getAssociationFields()->filter(function (AssociationField $associationField): bool {
  254. return $associationField instanceof OneToManyAssociationField;
  255. })->map(function (AssociationField $field): string {
  256. return $field->getReferenceDefinition()->getEntityName();
  257. });
  258. }
  259. private function getAssociationDefinitionByEntity(CompiledFieldCollection $collection, string $entityName): ?EntityDefinition
  260. {
  261. /** @var AssociationField|null $field */
  262. $field = $collection->filter(function (AssociationField $associationField) use ($entityName): bool {
  263. if (!$associationField instanceof OneToManyAssociationField) {
  264. return false;
  265. }
  266. return $associationField->getReferenceDefinition()->getEntityName() === $entityName;
  267. })->first();
  268. return $field ? $field->getReferenceDefinition() : null;
  269. }
  270. }