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

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