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

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