src/Core/Content/Rule/DataAbstractionLayer/RuleAreaUpdater.php line 79

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