src/Core/Content/Product/DataAbstractionLayer/ProductIndexer.php line 100

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\DataAbstractionLayer;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Product\Events\ProductIndexerEvent;
  5. use Shopware\Core\Content\Product\ProductDefinition;
  6. use Shopware\Core\Defaults;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\IterableQuery;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\IteratorFactory;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  10. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\ChildCountUpdater;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexer;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexerRegistry;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexingMessage;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\InheritanceUpdater;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\ManyToManyIdFieldUpdater;
  18. use Shopware\Core\Framework\Log\Package;
  19. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  20. use Shopware\Core\Framework\Uuid\Uuid;
  21. use Shopware\Core\Profiling\Profiler;
  22. use Symfony\Component\Messenger\MessageBusInterface;
  23. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  24. #[Package('core')]
  25. class ProductIndexer extends EntityIndexer
  26. {
  27.     final public const INHERITANCE_UPDATER 'product.inheritance';
  28.     final public const STOCK_UPDATER 'product.stock';
  29.     final public const VARIANT_LISTING_UPDATER 'product.variant-listing';
  30.     final public const CHILD_COUNT_UPDATER 'product.child-count';
  31.     final public const MANY_TO_MANY_ID_FIELD_UPDATER 'product.many-to-many-id-field';
  32.     final public const CATEGORY_DENORMALIZER_UPDATER 'product.category-denormalizer';
  33.     final public const CHEAPEST_PRICE_UPDATER 'product.cheapest-price';
  34.     final public const RATING_AVERAGE_UPDATER 'product.rating-average';
  35.     final public const STREAM_UPDATER 'product.stream';
  36.     final public const SEARCH_KEYWORD_UPDATER 'product.search-keyword';
  37.     final public const STATES_UPDATER 'product.states';
  38.     /**
  39.      * @internal
  40.      */
  41.     public function __construct(private readonly IteratorFactory $iteratorFactory, private readonly EntityRepository $repository, private readonly Connection $connection, private readonly VariantListingUpdater $variantListingUpdater, private readonly ProductCategoryDenormalizer $categoryDenormalizer, private readonly InheritanceUpdater $inheritanceUpdater, private readonly RatingAverageUpdater $ratingAverageUpdater, private readonly SearchKeywordUpdater $searchKeywordUpdater, private readonly ChildCountUpdater $childCountUpdater, private readonly ManyToManyIdFieldUpdater $manyToManyIdFieldUpdater, private readonly StockUpdater $stockUpdater, private readonly EventDispatcherInterface $eventDispatcher, private readonly CheapestPriceUpdater $cheapestPriceUpdater, private readonly ProductStreamUpdater $streamUpdater, private readonly StatesUpdater $statesUpdater, private readonly MessageBusInterface $messageBus)
  42.     {
  43.     }
  44.     public function getName(): string
  45.     {
  46.         return 'product.indexer';
  47.     }
  48.     /**
  49.      * @param array{offset: int|null}|null $offset
  50.      */
  51.     public function iterate(?array $offset): ?EntityIndexingMessage
  52.     {
  53.         $iterator $this->getIterator($offset);
  54.         $ids $iterator->fetch();
  55.         if (empty($ids)) {
  56.             return null;
  57.         }
  58.         return new ProductIndexingMessage(array_values($ids), $iterator->getOffset());
  59.     }
  60.     public function update(EntityWrittenContainerEvent $event): ?EntityIndexingMessage
  61.     {
  62.         $updates $event->getPrimaryKeys(ProductDefinition::ENTITY_NAME);
  63.         if (empty($updates)) {
  64.             return null;
  65.         }
  66.         Profiler::trace('product:indexer:inheritance', function () use ($updates$event): void {
  67.             $this->inheritanceUpdater->update(ProductDefinition::ENTITY_NAME$updates$event->getContext());
  68.         });
  69.         $stocks $event->getPrimaryKeysWithPropertyChange(ProductDefinition::ENTITY_NAME, ['stock''isCloseout''minPurchase']);
  70.         Profiler::trace('product:indexer:stock', function () use ($stocks$event): void {
  71.             $this->stockUpdater->update(array_values($stocks), $event->getContext());
  72.         });
  73.         $message = new ProductIndexingMessage(array_values($updates), null$event->getContext());
  74.         $message->addSkip(self::INHERITANCE_UPDATERself::STOCK_UPDATER);
  75.         $delayed \array_unique(\array_filter(\array_merge(
  76.             $this->getParentIds($updates),
  77.             $this->getChildrenIds($updates)
  78.         )));
  79.         foreach (\array_chunk($delayed50) as $chunk) {
  80.             $child = new ProductIndexingMessage($chunknull$event->getContext());
  81.             $child->setIndexer($this->getName());
  82.             EntityIndexerRegistry::addSkips($child$event->getContext());
  83.             $this->messageBus->dispatch($child);
  84.         }
  85.         return $message;
  86.     }
  87.     public function getTotal(): int
  88.     {
  89.         return $this->getIterator(null)->fetchCount();
  90.     }
  91.     public function getDecorated(): EntityIndexer
  92.     {
  93.         throw new DecorationPatternException(self::class);
  94.     }
  95.     public function handle(EntityIndexingMessage $message): void
  96.     {
  97.         $ids array_values(array_unique(array_filter($message->getData())));
  98.         if (empty($ids)) {
  99.             return;
  100.         }
  101.         $parentIds $this->filterVariants($ids);
  102.         $context $message->getContext();
  103.         if ($message->allow(self::INHERITANCE_UPDATER)) {
  104.             Profiler::trace('product:indexer:inheritance', function () use ($ids$context): void {
  105.                 $this->inheritanceUpdater->update(ProductDefinition::ENTITY_NAME$ids$context);
  106.             });
  107.         }
  108.         if ($message->allow(self::STOCK_UPDATER)) {
  109.             Profiler::trace('product:indexer:stock', function () use ($ids$context): void {
  110.                 $this->stockUpdater->update($ids$context);
  111.             });
  112.         }
  113.         if ($message->allow(self::VARIANT_LISTING_UPDATER)) {
  114.             Profiler::trace('product:indexer:variant-listing', function () use ($parentIds$context): void {
  115.                 $this->variantListingUpdater->update($parentIds$context);
  116.             });
  117.         }
  118.         if ($message->allow(self::CHILD_COUNT_UPDATER)) {
  119.             Profiler::trace('product:indexer:child-count', function () use ($parentIds$context): void {
  120.                 $this->childCountUpdater->update(ProductDefinition::ENTITY_NAME$parentIds$context);
  121.             });
  122.         }
  123.         if ($message->allow(self::STREAM_UPDATER)) {
  124.             Profiler::trace('product:indexer:streams', function () use ($ids$context): void {
  125.                 $this->streamUpdater->updateProducts($ids$context);
  126.             });
  127.         }
  128.         if ($message->allow(self::MANY_TO_MANY_ID_FIELD_UPDATER)) {
  129.             Profiler::trace('product:indexer:many-to-many', function () use ($ids$context): void {
  130.                 $this->manyToManyIdFieldUpdater->update(ProductDefinition::ENTITY_NAME$ids$context);
  131.             });
  132.         }
  133.         if ($message->allow(self::CATEGORY_DENORMALIZER_UPDATER)) {
  134.             Profiler::trace('product:indexer:category', function () use ($ids$context): void {
  135.                 $this->categoryDenormalizer->update($ids$context);
  136.             });
  137.         }
  138.         if ($message->allow(self::CHEAPEST_PRICE_UPDATER)) {
  139.             Profiler::trace('product:indexer:cheapest-price', function () use ($parentIds$context): void {
  140.                 $this->cheapestPriceUpdater->update($parentIds$context);
  141.             });
  142.         }
  143.         if ($message->allow(self::RATING_AVERAGE_UPDATER)) {
  144.             Profiler::trace('product:indexer:rating', function () use ($parentIds$context): void {
  145.                 $this->ratingAverageUpdater->update($parentIds$context);
  146.             });
  147.         }
  148.         if ($message->allow(self::SEARCH_KEYWORD_UPDATER)) {
  149.             Profiler::trace('product:indexer:search-keywords', function () use ($ids$context): void {
  150.                 $this->searchKeywordUpdater->update($ids$context);
  151.             });
  152.         }
  153.         if ($message->allow(self::STATES_UPDATER)) {
  154.             Profiler::trace('product:indexer:states', function () use ($ids$context): void {
  155.                 $this->statesUpdater->update($ids$context);
  156.             });
  157.         }
  158.         RetryableQuery::retryable($this->connection, function () use ($ids): void {
  159.             $this->connection->executeStatement(
  160.                 'UPDATE product SET updated_at = :now WHERE id IN (:ids)',
  161.                 ['ids' => Uuid::fromHexToBytesList($ids), 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT)],
  162.                 ['ids' => Connection::PARAM_STR_ARRAY]
  163.             );
  164.         });
  165.         Profiler::trace('product:indexer:event', function () use ($ids$context$message): void {
  166.             $this->eventDispatcher->dispatch(new ProductIndexerEvent($ids$context$message->getSkip()));
  167.         });
  168.     }
  169.     /**
  170.      * @return string[]
  171.      */
  172.     public function getOptions(): array
  173.     {
  174.         return [
  175.             self::INHERITANCE_UPDATER,
  176.             self::STOCK_UPDATER,
  177.             self::VARIANT_LISTING_UPDATER,
  178.             self::CHILD_COUNT_UPDATER,
  179.             self::MANY_TO_MANY_ID_FIELD_UPDATER,
  180.             self::CATEGORY_DENORMALIZER_UPDATER,
  181.             self::CHEAPEST_PRICE_UPDATER,
  182.             self::RATING_AVERAGE_UPDATER,
  183.             self::STREAM_UPDATER,
  184.             self::SEARCH_KEYWORD_UPDATER,
  185.         ];
  186.     }
  187.     /**
  188.      * @param array<string> $ids
  189.      *
  190.      * @return array<string>
  191.      */
  192.     private function getChildrenIds(array $ids): array
  193.     {
  194.         $childrenIds $this->connection->fetchFirstColumn(
  195.             'SELECT DISTINCT LOWER(HEX(id)) as id FROM product WHERE parent_id IN (:ids)',
  196.             ['ids' => Uuid::fromHexToBytesList($ids)],
  197.             ['ids' => Connection::PARAM_STR_ARRAY]
  198.         );
  199.         return array_unique(array_filter($childrenIds));
  200.     }
  201.     /**
  202.      * @param array<string> $ids
  203.      *
  204.      * @return string[]
  205.      */
  206.     private function getParentIds(array $ids): array
  207.     {
  208.         $parentIds $this->connection->fetchFirstColumn(
  209.             'SELECT DISTINCT LOWER(HEX(product.parent_id)) as id FROM product WHERE id IN (:ids)',
  210.             ['ids' => Uuid::fromHexToBytesList($ids)],
  211.             ['ids' => Connection::PARAM_STR_ARRAY]
  212.         );
  213.         return array_unique(array_filter($parentIds));
  214.     }
  215.     /**
  216.      * @param array<string> $ids
  217.      *
  218.      * @return array|mixed[]
  219.      */
  220.     private function filterVariants(array $ids): array
  221.     {
  222.         return $this->connection->fetchFirstColumn(
  223.             'SELECT DISTINCT LOWER(HEX(`id`))
  224.              FROM product
  225.              WHERE `id` IN (:ids)
  226.              AND `parent_id` IS NULL',
  227.             ['ids' => Uuid::fromHexToBytesList($ids)],
  228.             ['ids' => Connection::PARAM_STR_ARRAY]
  229.         );
  230.     }
  231.     /**
  232.      * @param array{offset: int|null}|null $offset
  233.      */
  234.     private function getIterator(?array $offset): IterableQuery
  235.     {
  236.         return $this->iteratorFactory->createIterator($this->repository->getDefinition(), $offset);
  237.     }
  238. }