vendor/shopware/core/Content/Product/SalesChannel/Listing/ProductListingLoader.php line 161

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\SalesChannel\Listing;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Product\Events\ProductListingPreviewCriteriaEvent;
  5. use Shopware\Core\Content\Product\Events\ProductListingResolvePreviewEvent;
  6. use Shopware\Core\Content\Product\ProductCollection;
  7. use Shopware\Core\Content\Product\ProductDefinition;
  8. use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
  9. use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Struct\ArrayEntity;
  19. use Shopware\Core\Framework\Uuid\Uuid;
  20. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
  21. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  22. use Shopware\Core\System\SystemConfig\SystemConfigService;
  23. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  24. #[Package('inventory')]
  25. class ProductListingLoader
  26. {
  27.     /**
  28.      * @internal
  29.      */
  30.     public function __construct(private readonly SalesChannelRepository $repository, private readonly SystemConfigService $systemConfigService, private readonly Connection $connection, private readonly EventDispatcherInterface $eventDispatcher, private readonly AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory)
  31.     {
  32.     }
  33.     public function load(Criteria $originSalesChannelContext $context): EntitySearchResult
  34.     {
  35.         $origin->addState(Criteria::STATE_ELASTICSEARCH_AWARE);
  36.         $criteria = clone $origin;
  37.         $this->addGrouping($criteria);
  38.         $this->handleAvailableStock($criteria$context);
  39.         $ids $this->repository->searchIds($criteria$context);
  40.         /** @var list<string> $keys */
  41.         $keys $ids->getIds();
  42.         $aggregations $this->repository->aggregate($criteria$context);
  43.         // no products found, no need to continue
  44.         if (empty($keys)) {
  45.             return new EntitySearchResult(
  46.                 ProductDefinition::ENTITY_NAME,
  47.                 0,
  48.                 new ProductCollection(),
  49.                 $aggregations,
  50.                 $origin,
  51.                 $context->getContext()
  52.             );
  53.         }
  54.         $mapping array_combine($keys$keys);
  55.         $hasOptionFilter $this->hasOptionFilter($criteria);
  56.         if (!$hasOptionFilter) {
  57.             $mapping $this->resolvePreviews($keys$context);
  58.         }
  59.         $event = new ProductListingResolvePreviewEvent($context$criteria$mapping$hasOptionFilter);
  60.         $this->eventDispatcher->dispatch($event);
  61.         $mapping $event->getMapping();
  62.         $read $criteria->cloneForRead(array_values($mapping));
  63.         $read->addAssociation('options.group');
  64.         $entities $this->repository->search($read$context);
  65.         $this->addExtensions($ids$entities$mapping);
  66.         $result = new EntitySearchResult(ProductDefinition::ENTITY_NAME$ids->getTotal(), $entities->getEntities(), $aggregations$origin$context->getContext());
  67.         $result->addState(...$ids->getStates());
  68.         return $result;
  69.     }
  70.     private function hasOptionFilter(Criteria $criteria): bool
  71.     {
  72.         $filters $criteria->getPostFilters();
  73.         $fields = [];
  74.         foreach ($filters as $filter) {
  75.             array_push($fields, ...$filter->getFields());
  76.         }
  77.         $fields array_map(fn (string $field) => preg_replace('/^product./'''$field), $fields);
  78.         if (\in_array('options.id'$fieldstrue)) {
  79.             return true;
  80.         }
  81.         if (\in_array('optionIds'$fieldstrue)) {
  82.             return true;
  83.         }
  84.         return false;
  85.     }
  86.     private function addGrouping(Criteria $criteria): void
  87.     {
  88.         $criteria->addGroupField(new FieldGrouping('displayGroup'));
  89.         $criteria->addFilter(
  90.             new NotFilter(
  91.                 NotFilter::CONNECTION_AND,
  92.                 [new EqualsFilter('displayGroup'null)]
  93.             )
  94.         );
  95.     }
  96.     private function handleAvailableStock(Criteria $criteriaSalesChannelContext $context): void
  97.     {
  98.         $salesChannelId $context->getSalesChannel()->getId();
  99.         $hide $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock'$salesChannelId);
  100.         if (!$hide) {
  101.             return;
  102.         }
  103.         $closeoutFilter $this->productCloseoutFilterFactory->create($context);
  104.         $criteria->addFilter($closeoutFilter);
  105.     }
  106.     /**
  107.      * @param array<string> $ids
  108.      *
  109.      * @throws \JsonException
  110.      *
  111.      * @return array<string>
  112.      */
  113.     private function resolvePreviews(array $idsSalesChannelContext $context): array
  114.     {
  115.         $ids array_combine($ids$ids);
  116.         $config $this->connection->fetchAllAssociative(
  117.             '# product-listing-loader::resolve-previews
  118.             SELECT
  119.                 parent.variant_listing_config as variantListingConfig,
  120.                 LOWER(HEX(child.id)) as id,
  121.                 LOWER(HEX(parent.id)) as parentId
  122.              FROM product as child
  123.                 INNER JOIN product as parent
  124.                     ON parent.id = child.parent_id
  125.                     AND parent.version_id = child.version_id
  126.              WHERE child.version_id = :version
  127.              AND child.id IN (:ids)',
  128.             [
  129.                 'ids' => Uuid::fromHexToBytesList(array_values($ids)),
  130.                 'version' => Uuid::fromHexToBytes($context->getContext()->getVersionId()),
  131.             ],
  132.             ['ids' => Connection::PARAM_STR_ARRAY]
  133.         );
  134.         $mapping = [];
  135.         foreach ($config as $item) {
  136.             if ($item['variantListingConfig'] === null) {
  137.                 continue;
  138.             }
  139.             $variantListingConfig json_decode((string) $item['variantListingConfig'], true512\JSON_THROW_ON_ERROR);
  140.             if (isset($variantListingConfig['mainVariantId']) && $variantListingConfig['mainVariantId']) {
  141.                 $mapping[$item['id']] = $variantListingConfig['mainVariantId'];
  142.             }
  143.             if (isset($variantListingConfig['displayParent']) && $variantListingConfig['displayParent']) {
  144.                 $mapping[$item['id']] = $item['parentId'];
  145.             }
  146.         }
  147.         // now we have a mapping for "child => main variant"
  148.         if (empty($mapping)) {
  149.             return $ids;
  150.         }
  151.         // filter inactive and not available variants
  152.         $criteria = new Criteria(array_values($mapping));
  153.         $criteria->addFilter(new ProductAvailableFilter($context->getSalesChannel()->getId()));
  154.         $this->handleAvailableStock($criteria$context);
  155.         $this->eventDispatcher->dispatch(
  156.             new ProductListingPreviewCriteriaEvent($criteria$context)
  157.         );
  158.         $available $this->repository->searchIds($criteria$context);
  159.         $remapped = [];
  160.         // replace existing ids with main variant id
  161.         foreach ($ids as $id) {
  162.             // id has no mapped main_variant - keep old id
  163.             if (!isset($mapping[$id])) {
  164.                 $remapped[$id] = $id;
  165.                 continue;
  166.             }
  167.             // get access to main variant id over the fetched config mapping
  168.             $main $mapping[$id];
  169.             // main variant is configured but not active/available - keep old id
  170.             if (!$available->has($main)) {
  171.                 $remapped[$id] = $id;
  172.                 continue;
  173.             }
  174.             // main variant is configured and available - add main variant id
  175.             $remapped[$id] = $main;
  176.         }
  177.         return $remapped;
  178.     }
  179.     /**
  180.      * @param array<string> $mapping
  181.      */
  182.     private function addExtensions(IdSearchResult $idsEntitySearchResult $entities, array $mapping): void
  183.     {
  184.         foreach ($ids->getExtensions() as $name => $extension) {
  185.             $entities->addExtension($name$extension);
  186.         }
  187.         /** @var string $id */
  188.         foreach ($ids->getIds() as $id) {
  189.             if (!isset($mapping[$id])) {
  190.                 continue;
  191.             }
  192.             // current id was mapped to another variant
  193.             if (!$entities->has($mapping[$id])) {
  194.                 continue;
  195.             }
  196.             /** @var Entity $entity */
  197.             $entity $entities->get($mapping[$id]);
  198.             // get access to the data of the search result
  199.             $entity->addExtension('search', new ArrayEntity($ids->getDataOfId($id)));
  200.         }
  201.     }
  202. }