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

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$associatedEntitiestrue)) {
  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 $fieldsEntityWrittenEvent $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 $idsCompiledFieldCollection $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 $queryAssociationField $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 $collectionstring $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. }