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

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