custom/plugins/MegaParentProductListing/src/Components/Product/SalesChannel/Listing/ProductListingLoader.php line 68

Open in your IDE?
  1. <?php
  2. /**
  3.  * @author Christian Reinelt <c.reinelt@mediagraphik.de>
  4.  * @copyright (c) Mediagraphik GmbH
  5.  */
  6. namespace MegaParentProductListing\Components\Product\SalesChannel\Listing;
  7. use MegaParentProductListing\Subscriber\CrossSellingSubscriber;
  8. use Shopware\Core\Content\Product\Events\ProductListingPreviewCriteriaEvent;
  9. use Shopware\Core\Content\Product\ProductDefinition;
  10. use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingLoader as OriginalProductListingLoader;
  11. use Doctrine\DBAL\Connection;
  12. use Shopware\Core\Content\Product\ProductCollection;
  13. use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
  14. use Shopware\Core\Content\Product\SalesChannel\ProductCloseoutFilter;
  15. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  24. use Shopware\Core\Framework\Struct\ArrayEntity;
  25. use Shopware\Core\Framework\Uuid\Uuid;
  26. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  27. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  28. use Shopware\Core\System\SystemConfig\SystemConfigService;
  29. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  30. class ProductListingLoader extends OriginalProductListingLoader
  31. {
  32.     /**
  33.      * @var SalesChannelRepositoryInterface
  34.      */
  35.     private $repository;
  36.     /**
  37.      * @var SystemConfigService
  38.      */
  39.     private $systemConfigService;
  40.     /**
  41.      * @var Connection
  42.      */
  43.     private $connection;
  44.     /**
  45.      * @var EventDispatcherInterface
  46.      */
  47.     private $eventDispatcher;
  48.     public function __construct(
  49.         SalesChannelRepositoryInterface $repository,
  50.         SystemConfigService             $systemConfigService,
  51.         Connection                      $connection,
  52.         EventDispatcherInterface        $eventDispatcher
  53.     )
  54.     {
  55.         $this->repository $repository;
  56.         $this->systemConfigService $systemConfigService;
  57.         $this->connection $connection;
  58.         $this->eventDispatcher $eventDispatcher;
  59.     }
  60.     public function load(Criteria $originSalesChannelContext $context): EntitySearchResult
  61.     {
  62.         $criteria = clone $origin;
  63.         $this->handleAvailableStock($criteria$context);
  64.         $aggregationCriteria = clone $criteria;
  65.         $this->addGrouping($aggregationCriteria);
  66.         $this->addGrouping($criteria);
  67.         if (!$this->shouldApplyParentFilter($criteria$context)) {
  68.             $idResult $this->repository->searchIds($criteria$context);
  69.             $ids $idResult;
  70.         } else {
  71.             $idResult $this->repository->search($criteria$context);
  72.             $ids = [];
  73.             /** @var SalesChannelProductEntity $product */
  74.             foreach ($idResult as $product) {
  75.                 $id = !empty($product->getParentId()) ? $product->getParentId() : $product->getId();
  76.                 $data = ['id' => $id'_score' => 0];
  77.                 if ($product->hasExtension('search')) {
  78.                     /** @var ArrayEntity $search */
  79.                     $search $product->getExtension('search');
  80.                     if ($search->has('_score')) {
  81.                         $data['_score'] = $search->get('_score');
  82.                     }
  83.                 }
  84.                 $ids[$id] = ['primaryKey' => $id'data' => $data];
  85.             }
  86.             $ids = new IdSearchResult(count($ids), $ids$criteria$context->getContext());
  87.         }
  88.         $aggregations $this->repository->aggregate($aggregationCriteria$context);
  89.         // no products found, no need to continue
  90.         if (empty($ids->getIds())) {
  91.             return new EntitySearchResult(
  92.                 ProductDefinition::ENTITY_NAME,
  93.                 0,
  94.                 new ProductCollection(),
  95.                 $aggregations,
  96.                 $origin,
  97.                 $context->getContext()
  98.             );
  99.         }
  100.         $variantIds $ids->getIds();
  101.         $mapping array_combine($ids->getIds(), $ids->getIds());
  102.         if (!$this->hasOptionFilter($criteria)) {
  103.             list($variantIds$mapping) = $this->resolvePreviews($ids->getIds(), $context);
  104.         }
  105.         $read $criteria->cloneForRead($variantIds);
  106.         $read->addAssociation('options.group');
  107.         $entities $this->repository->search($read$context);
  108.         $this->addExtensions($ids$entities$mapping);
  109.         $result = new EntitySearchResult(ProductDefinition::ENTITY_NAME$idResult->getTotal(), $entities->getEntities(), $aggregations$origin$context->getContext());
  110.         if (method_exists($result'getStates')) {
  111.             $result->addState(...$ids->getStates());
  112.         }
  113.         return $result;
  114.     }
  115.     private function hasOptionFilter(Criteria $criteria): bool
  116.     {
  117.         $fields $criteria->getFilterFields();
  118.         $fields array_map(function (string $field) {
  119.             return preg_replace('/^product./'''$field);
  120.         }, $fields);
  121.         if (\in_array('options.id'$fieldstrue)) {
  122.             return true;
  123.         }
  124.         if (\in_array('optionIds'$fieldstrue)) {
  125.             return true;
  126.         }
  127.         return false;
  128.     }
  129.     private function handleAvailableStock(Criteria $criteriaSalesChannelContext $context): void
  130.     {
  131.         $salesChannelId $context->getSalesChannel()->getId();
  132.         $hide $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock'$salesChannelId);
  133.         if (!$hide) {
  134.             return;
  135.         }
  136.         $criteria->addFilter(new ProductCloseoutFilter());
  137.     }
  138.     private function resolvePreviews(array $idsSalesChannelContext $context): array
  139.     {
  140.         $ids array_combine($ids$ids);
  141.         $config $this->connection->fetchAll(
  142.             '# product-listing-loader::resolve-previews
  143.             SELECT parent.configurator_group_config,
  144.                         LOWER(HEX(parent.main_variant_id)) as mainVariantId,
  145.                         LOWER(HEX(child.id)) as id
  146.              FROM product as child
  147.                 INNER JOIN product as parent
  148.                     ON parent.id = child.parent_id
  149.                     AND parent.version_id = child.version_id
  150.              WHERE child.version_id = :version
  151.              AND child.id IN (:ids)',
  152.             [
  153.                 'ids' => Uuid::fromHexToBytesList(array_values($ids)),
  154.                 'version' => Uuid::fromHexToBytes($context->getContext()->getVersionId()),
  155.             ],
  156.             ['ids' => Connection::PARAM_STR_ARRAY]
  157.         );
  158.         $mapping = [];
  159.         foreach ($config as $item) {
  160.             if ($item['mainVariantId']) {
  161.                 $mapping[$item['id']] = $item['mainVariantId'];
  162.             }
  163.         }
  164.         // now we have a mapping for "child => main variant"
  165.         if (empty($mapping)) {
  166.             return [$idsarray_combine($ids$ids)];
  167.         }
  168.         // filter inactive and not available variants
  169.         $criteria = new Criteria(array_values($mapping));
  170.         $criteria->addFilter(new ProductAvailableFilter($context->getSalesChannel()->getId()));
  171.         $this->handleAvailableStock($criteria$context);
  172.         $this->eventDispatcher->dispatch(
  173.             new ProductListingPreviewCriteriaEvent($criteria$context)
  174.         );
  175.         $available $this->repository->searchIds($criteria$context);
  176.         $remapped = [];
  177.         // replace existing ids with main variant id
  178.         $sorted = [];
  179.         foreach ($ids as $id) {
  180.             // id has no mapped main_variant - keep old id
  181.             if (!isset($mapping[$id])) {
  182.                 $sorted[] = $id;
  183.                 $remapped[$id] = $id;
  184.                 continue;
  185.             }
  186.             // get access to main variant id over the fetched config mapping
  187.             $main $mapping[$id];
  188.             // main variant is configured but not active/available - keep old id
  189.             if (!$available->has($main)) {
  190.                 $sorted[] = $id;
  191.                 $remapped[$id] = $id;
  192.                 continue;
  193.             }
  194.             // main variant is configured and available - add main variant id
  195.             if (!\in_array($main$sortedtrue)) {
  196.                 $remapped[$id] = $main;
  197.                 $sorted[] = $main;
  198.             }
  199.         }
  200.         return [$sorted$remapped];
  201.     }
  202.     private function addExtensions(IdSearchResult $idsEntitySearchResult $entities, array $mapping): void
  203.     {
  204.         foreach ($ids->getExtensions() as $name => $extension) {
  205.             $entities->addExtension($name$extension);
  206.         }
  207.         foreach ($ids->getIds() as $id) {
  208.             if (!isset($mapping[$id])) {
  209.                 continue;
  210.             }
  211.             // current id was mapped to another variant
  212.             if (!$entities->has($mapping[$id])) {
  213.                 continue;
  214.             }
  215.             /** @var Entity $entity */
  216.             $entity $entities->get($mapping[$id]);
  217.             // get access to the data of the search result
  218.             $entity->addExtension('search', new ArrayEntity($ids->getDataOfId($id)));
  219.         }
  220.     }
  221.     private function addGrouping(Criteria $criteria): void
  222.     {
  223.         $criteria->addGroupField(new FieldGrouping('displayGroup'));
  224.         $criteria->addFilter(
  225.             new NotFilter(
  226.                 NotFilter::CONNECTION_AND,
  227.                 [new EqualsFilter('displayGroup'null)]
  228.             )
  229.         );
  230.     }
  231.     private function shouldApplyParentFilter(Criteria $criteriaSalesChannelContext $context): bool
  232.     {
  233.         $config $this->systemConfigService->get('MegaParentProductListing.config'$context->getSalesChannelId());
  234.         $enableForSearch $config['enableForSearch'] ?? false;
  235.         $disableForDynamicProductGroups $config['disableForDynamicProductGroups'] ?? false;
  236.         $enableForFilteredListing $config['enableForFilteredListing'] ?? false;
  237.         if ($this->isProductSearch($criteria$context)) {
  238.             return $enableForSearch && !$this->criteriaIsFiltered($criteria$enableForFilteredListing);
  239.         }
  240.         if (!$this->isCategoryListing($criteria)) {
  241.             if ($criteria->hasState(CrossSellingSubscriber::CROSS_SELLING_CRITERIA_STATE)) {
  242.                 return true;
  243.             }
  244.             return !$disableForDynamicProductGroups && !$this->criteriaIsFiltered($criteria$enableForFilteredListing);
  245.         }
  246.         return !$this->criteriaIsFiltered($criteria$enableForFilteredListing);
  247.     }
  248.     private function criteriaIsFiltered(Criteria $criteriabool $enabled): bool
  249.     {
  250.         if (!$enabled) {
  251.             return false;
  252.         }
  253.         if (empty($criteria->getPostFilters())) {
  254.             return false;
  255.         }
  256.         foreach ($criteria->getPostFilters() as $postFilter) {
  257.             if (!$postFilter instanceof MultiFilter) {
  258.                 continue;
  259.             }
  260.             return true;
  261.         }
  262.         return false;
  263.     }
  264.     private function isCategoryListing(Criteria $criteria): bool
  265.     {
  266.         if ($criteria->getTitle() === 'cms::product-listing') {
  267.             return true;
  268.         }
  269.         foreach ($criteria->getFilters() as $filter) {
  270.             if (!$filter instanceof EqualsFilter) {
  271.                 continue;
  272.             }
  273.             if ($filter->getField() === 'product.categoriesRo.id') {
  274.                 return true;
  275.             }
  276.         }
  277.         return false;
  278.     }
  279.     private function isProductSearch(Criteria $criteriaSalesChannelContext $context): bool
  280.     {
  281.         if ($criteria->getTitle() === 'cms::product-listing') {
  282.             return false;
  283.         }
  284.         if ($criteria->getTitle() === 'search-page') {
  285.             return true;
  286.         }
  287.         if ($context->getContext()->hasState('elasticsearchAware')) {
  288.             return true;
  289.         }
  290.         if (empty($criteria->getPostFilters())) {
  291.             return false;
  292.         }
  293.         foreach ($criteria->getPostFilters() as $postFilter) {
  294.             if (!$postFilter instanceof MultiFilter) {
  295.                 continue;
  296.             }
  297.             return true;
  298.         }
  299.         return false;
  300.     }
  301. }