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

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