custom/plugins/MaxiaListingVariants6/src/Service/ListingVariantsLoader.php line 237

Open in your IDE?
  1. <?php
  2. namespace Maxia\MaxiaListingVariants6\Service;
  3. use Maxia\MaxiaListingVariants6\Core\Content\Product\Cms\ProductListingCmsElementResolver;
  4. use Maxia\MaxiaListingVariants6\Events\ListingVariantsBeforeLoadEvent;
  5. use Maxia\MaxiaListingVariants6\Events\ResolvePreselectionsEvent;
  6. use Monolog\Logger;
  7. use Doctrine\DBAL\Connection;
  8. use Maxia\MaxiaListingVariants6\Config\BaseConfig;
  9. use Maxia\MaxiaListingVariants6\Config\ProductConfig;
  10. use Maxia\MaxiaListingVariants6\Config\PropertyGroupConfig;
  11. use Maxia\MaxiaListingVariants6\Events\ListingVariantsLoadedEvent;
  12. use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
  13. use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
  14. use Shopware\Core\Content\Product\ProductEntity;
  15. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  16. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
  17. use Shopware\Core\Content\Property\PropertyGroupCollection;
  18. use Shopware\Core\Content\Property\PropertyGroupEntity;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  20. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  25. use Shopware\Core\Framework\Uuid\Uuid;
  26. use Shopware\Core\PlatformRequest;
  27. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  28. use Symfony\Component\DependencyInjection\ContainerInterface;
  29. use Symfony\Component\HttpFoundation\Request;
  30. use Symfony\Component\HttpFoundation\RequestStack;
  31. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  32. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  33. class ListingVariantsLoader implements ListingVariantsLoaderInterface
  34. {
  35.     /** @var ContainerInterface */
  36.     protected $container;
  37.     /** @var Logger */
  38.     protected $logger;
  39.     /** @var RequestStack */
  40.     protected $requestStack;
  41.     /** @var EventDispatcherInterface */
  42.     protected $eventDispatcher;
  43.     /** @var Connection */
  44.     protected $dbConnection;
  45.     /** @var ProductConfiguratorLoaderInterface */
  46.     protected $configuratorLoader;
  47.     /** @var VariantMappingLoaderInterface */
  48.     protected $variantMappingLoader;
  49.     /** @var ProductCombinationFinder */
  50.     protected $combinationFinder;
  51.     /** @var SalesChannelRepositoryInterface */
  52.     protected $productRepository;
  53.     /** @var EntityRepository */
  54.     protected $mediaRepository;
  55.     /** @var EntityRepository */
  56.     protected $productMediaRepository;
  57.     /** @var VariantDisplayConfigLoader */
  58.     protected $variantDisplayConfigLoader;
  59.     /** @var ConfigService */
  60.     protected $configService;
  61.     public function __construct(
  62.         ContainerInterface $container,
  63.         Logger $logger,
  64.         RequestStack $requestStack,
  65.         EventDispatcherInterface $eventDispatcher,
  66.         Connection $dbConnection,
  67.         ProductConfiguratorLoaderInterface $configuratorLoader,
  68.         VariantMappingLoaderInterface $variantMappingLoader,
  69.         ProductCombinationFinderInterface $combinationFinder,
  70.         $productRepository,
  71.         $mediaRepository,
  72.         $productMediaRepository,
  73.         VariantDisplayConfigLoader $variantDisplayConfigLoader,
  74.         ConfigService $configService
  75.     ) {
  76.         $this->container $container;
  77.         $this->logger $logger;
  78.         $this->requestStack $requestStack;
  79.         $this->eventDispatcher $eventDispatcher;
  80.         $this->dbConnection $dbConnection;
  81.         $this->configuratorLoader $configuratorLoader;
  82.         $this->variantMappingLoader $variantMappingLoader;
  83.         $this->combinationFinder $combinationFinder;
  84.         $this->productRepository $productRepository;
  85.         $this->productMediaRepository $productMediaRepository;
  86.         $this->mediaRepository $mediaRepository;
  87.         $this->variantDisplayConfigLoader $variantDisplayConfigLoader;
  88.         $this->configService $configService;
  89.     }
  90.     /**
  91.      * @param SalesChannelProductEntity[] $products
  92.      */
  93.     public function load(array $productsSalesChannelContext $context$expandOptions null)
  94.     {
  95.         $request $this->getMainRequest();
  96.         $pluginConfig $this->configService->getBaseConfig($context);
  97.         if ($expandOptions === null) {
  98.             $expandOptions $request $request->query->get('expandOptions'false) : false;
  99.         }
  100.         // add variants config extension to all products and merge with defaults
  101.         foreach ($products as $product) {
  102.             $isVariantProduct = !empty($product->getParentId()) || $product->getChildCount();
  103.             if (!$product->hasExtension('maxiaListingVariants')) {
  104.                 $config $this->configService->getProductConfig($productnull$context);
  105.                 $product->addExtension('maxiaListingVariants'$config);
  106.             }
  107.             $config $product->getExtension('maxiaListingVariants');
  108.             
  109.             if ($isVariantProduct) {
  110.                 if ($config->isQuickBuyActive() && $config->getDisplayMode() === 'none') {
  111.                     if ($expandOptions) {
  112.                         $config->setDisplayMode('all');
  113.                     } else {
  114.                         $config->setIsPartialConfiguration(true);
  115.                     }
  116.                 }
  117.             } else {
  118.                 // disable for main products
  119.                 $config->setDisplayMode('none');
  120.                 $config->setQuickBuyActive(
  121.                     $pluginConfig->isQuickBuyActive() && $pluginConfig->isActivateForMainProducts()
  122.                 );
  123.             }
  124.         }
  125.         // filter products where no additional data needs to be loaded
  126.         $products array_filter($products, function($product) {
  127.             return $product->hasExtension('maxiaListingVariants')
  128.                 && $product->getExtension('maxiaListingVariants')->getDisplayMode() !== 'none';
  129.         });
  130.         if (!$products) {
  131.             return;
  132.         }
  133.         // get display configs / preselection info
  134.         $displayConfigs $this->variantDisplayConfigLoader->load($products$context);
  135.         if ($request && $request->query->get('prependOptions')) {
  136.             $prependedOptions json_decode($request->query->get('prependOptions'), true);
  137.         }
  138.         // get first variant for all main products (required for loading the configurator)
  139.         $productIds = [];
  140.         $firstVariants = [];
  141.         foreach ($products as $product) {
  142.             if (!$product->getParentId() && $product->getChildCount()) {
  143.                 $productIds[] = $product->getId();
  144.             }
  145.         }
  146.         if ($productIds) {
  147.             $firstVariants $this->getFirstVariants($productIds$displayConfigs$context) ?: [];
  148.         }
  149.         // load configurator for each product
  150.         foreach ($products as $i => $product) {
  151.             /** @var ProductConfig $config */
  152.             $config $product->getExtension('maxiaListingVariants');
  153.             $config->setIsExpanded($expandOptions);
  154.             if ($config->isExpanded()) {
  155.                 $this->setExpandedConfig($pluginConfig$config);
  156.             }
  157.             if (isset($prependedOptions) && $prependedOptions) {
  158.                 $config->setPrependedOptions($prependedOptions);
  159.             }
  160.             if (!$product->getOptions()) {
  161.                 $product->setOptions(new PropertyGroupOptionCollection());
  162.             }
  163.             if (!$product->getOptionIds()) {
  164.                 $product->setOptionIds([]);
  165.             }
  166.             // load configurator
  167.             $this->eventDispatcher->dispatch(new ListingVariantsBeforeLoadEvent($product$config$context));
  168.             if (!$product->getParentId() && isset($firstVariants[$product->getId()])) {
  169.                 $settings $this->configuratorLoader->load($firstVariants[$product->getId()], $context);
  170.             } else {
  171.                 $settings $this->configuratorLoader->load($product$context);
  172.             }
  173.             if ($settings && $settings->count()) {
  174.                 $config->setTotalGroupCount($settings->count());
  175.                 $config->setOptions($settings);
  176.                 $config->setOptions($this->filterGroups($product$context));
  177.             } else {
  178.                 unset($products[$i]);
  179.             }
  180.         }
  181.         // handle preselection
  182.         if ($this->shouldHandlePreselection()) {
  183.             $this->handlePreselection($products$displayConfigs$context);
  184.         }
  185.         // load additional data, limit displayed options
  186.         foreach ($products as $product) {
  187.             /** @var ProductConfig $config */
  188.             $config $product->getExtension('maxiaListingVariants');
  189.             $settings $config->getOptions();
  190.             if (!$settings || !$settings->count()) {
  191.                 continue;
  192.             }
  193.             $settings $this->filterOptions($product$context);
  194.             $config->setOptions($settings);
  195.             // load option => product mappings
  196.             $mappings $this->variantMappingLoader->loadAllMappings($product$context);
  197.             // add 'loadEntity' to each mapping if needed
  198.             foreach ($settings->getElements() as $group) {
  199.                 /** @var PropertyGroupEntity $group */
  200.                 /** @var PropertyGroupConfig $groupConfig */
  201.                 $groupConfig $group->getExtension('maxiaListingVariants');
  202.                 if ($pluginConfig->isLoadAllEntities() ||
  203.                     ($groupConfig->isShowPrices() && in_array($groupConfig->getDisplayType(), ['dropdown''list']) ||
  204.                     ($groupConfig->isShowPrices() && in_array($groupConfig->getDisplayType(), ['inherited'null])
  205.                         && $group->getDisplayType() === 'select'))
  206.                 ) {
  207.                     foreach ($mappings as $optionId => $mapping) {
  208.                         if (in_array($optionId$group->getOptions()->getKeys())) {
  209.                             $mappings[$optionId]['loadEntity'] = true;
  210.                         }
  211.                     }
  212.                 }
  213.             }
  214.             $mappings $this->loadProductEntities($mappings$product$context);
  215.             if ($pluginConfig->isSwitchImageOnHover()) {
  216.                 $mappings $this->loadProductCovers($mappings$product$context);
  217.             }
  218.             $config->setOptionProductMappings($mappings);
  219.             // get preselection and save to config
  220.             $selection = [];
  221.             if ($product->getOptions()) {
  222.                 foreach ($product->getOptions()->getElements() as $optionEntity) {
  223.                     $selection[$optionEntity->getGroupId()] = $optionEntity->getId();
  224.                 }
  225.                 $config->setSelection($selection);
  226.             }
  227.             $this->eventDispatcher->dispatch(new ListingVariantsLoadedEvent($product$config$context));
  228.         }
  229.     }
  230.     /**
  231.      * Returns the first variant for all product ids
  232.      */
  233.     protected function getFirstVariants(array $parentIds, array $displayConfigsSalesChannelContext $context): ?array
  234.     {
  235.         $criteria = new Criteria();
  236.         $filterProductIds = [];
  237.         foreach ($parentIds as $parentId) {
  238.             if (isset($displayConfigs[$parentId])) {
  239.                 $displayConfig $displayConfigs[$parentId];
  240.                 if ($displayConfig['mainVariantId']) {
  241.                     $productId $displayConfig['mainVariantId'];
  242.                 } else {
  243.                     $productId $displayConfig['firstAvailableVariantId'] ?: $displayConfig['firstVariantId'];
  244.                 }
  245.                 $filterProductIds[] = $productId;
  246.             }
  247.         }
  248.         if (!$filterProductIds) return null;
  249.         $criteria->addFilter(new EqualsAnyFilter('id'$filterProductIds));
  250.         $criteria->addAssociation('options')
  251.             ->addAssociation('options.group');
  252.         $result $this->productRepository->search($criteria$context);
  253.         $products = [];
  254.         foreach ($result->getEntities() as $entity) {
  255.             $products[$entity->getParentId() ?: $entity->getId()] = $entity;
  256.         }
  257.         return $products;
  258.     }
  259.     /**
  260.      * Check if preselection should be handled for the current request.
  261.      */
  262.     protected function shouldHandlePreselection(): bool
  263.     {
  264.         $request $this->getMainRequest();
  265.         $hasPropertyFilter $request->query->get('properties') !== null;
  266.         $handlePreselectionAttr $request->attributes->get('maxia-handle-variant-preselection'true);
  267.         /** @var SalesChannelContext $context */
  268.         $context $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  269.         return $handlePreselectionAttr && !$hasPropertyFilter
  270.             && !$this->configService->getBaseConfig($context)->isDisablePreselection();
  271.     }
  272.     /**
  273.      * Overwrite config when popup mode is active
  274.      */
  275.     protected function setExpandedConfig(BaseConfig $baseConfigProductConfig $config): void
  276.     {
  277.         $config->setDisplayMode('all');
  278.         $config->setQuickBuyActive(true);
  279.         $baseConfig->setShowDeliveryInfo(true);
  280.     }
  281.     /**
  282.      * Loads the property group configs and removes groups that should be hidden in the listing.
  283.      */
  284.     protected function filterGroups(ProductEntity $productSalesChannelContext $context): PropertyGroupCollection
  285.     {
  286.         /** @var ProductConfig $config */
  287.         $config $product->getExtension('maxiaListingVariants');
  288.         $settings $config->getOptions();
  289.         $groupIndex = -1;
  290.         $removed 0;
  291.         // filter and extend options
  292.         foreach ($settings->getElements() as $index => $groupEntity) {
  293.             $groupIndex++;
  294.             // remove group if not allowed by config
  295.             if (!$this->configService->checkDisplayMode($config$groupEntity$groupIndex)) {
  296.                 $settings->remove($index);
  297.                 $removed++;
  298.                 continue;
  299.             }
  300.             // add property group config extension
  301.             $groupConfig $this->configService->getPropertyGroupConfig($groupEntity$context);
  302.             $groupEntity->addExtension('maxiaListingVariants'$groupConfig);
  303.             if ($groupConfig->getDisplayType() && $groupConfig->getDisplayType() !== 'inherited') {
  304.                 $groupEntity->setDisplayType($groupConfig->getDisplayType());
  305.             }
  306.         }
  307.         if ($removed) {
  308.             $config->setIsPartialConfiguration(true);
  309.         }
  310.         return $settings;
  311.     }
  312.     protected function filterOptions(ProductEntity $productSalesChannelContext $context): PropertyGroupCollection
  313.     {
  314.         /** @var ProductConfig $config */
  315.         $config $product->getExtension('maxiaListingVariants');
  316.         /** @var PropertyGroupCollection $settings */
  317.         $settings $config->getOptions();
  318.         $prependedOptions = new EntityCollection();
  319.         $hideUnavailable $this->configService->getBaseConfig($context)->isHideSoldOutCloseoutProducts()
  320.             && $config->getTotalGroupCount() === 1;
  321.         $requiredOptionIds array_unique(array_merge(
  322.             $product->getOptionIds() ?? [], $config->getPrependedOptions() ?? []
  323.         ));
  324.         // filter and extend options
  325.         foreach ($settings->getElements() as $groupEntity) {
  326.             /** @var PropertyGroupEntity $groupEntity */
  327.             /** @var PropertyGroupConfig $groupConfig */
  328.             $groupConfig $groupEntity->getExtension('maxiaListingVariants');
  329.             $newOptions = new PropertyGroupOptionCollection();
  330.             // remove out of stock options
  331.             if ($hideUnavailable) {
  332.                 $newOptions $groupEntity->getOptions()->filter(function($option) use ($requiredOptionIds) {
  333.                     return $option->getCombinable() || in_array($option->getId(), $requiredOptionIds);
  334.                 });
  335.             } else {
  336.                 $newOptions->merge($groupEntity->getOptions());
  337.             }
  338.             $groupConfig->setTotalEntries($newOptions->count());
  339.             if (!$config->isExpanded()) {
  340.                 // limit max options and remove unavailable options
  341.                 $optionIndex 0;
  342.                 foreach ($newOptions as $option) {
  343.                     $optionIndex++;
  344.                     if ($groupConfig->getMaxEntries() && $optionIndex $groupConfig->getMaxEntries()) {
  345.                         $newOptions->remove($option->getId());
  346.                     }
  347.                 }
  348.                 // prepend / append preselected option if it was cut off
  349.                 foreach ($requiredOptionIds as $requiredOptionId) {
  350.                     $options $newOptions->filter(function($option) use ($requiredOptionId) {
  351.                         return $requiredOptionId === $option->getId();
  352.                     });
  353.                     if ($options->count()) {
  354.                         // option is already in list
  355.                         continue;
  356.                     }
  357.                     $requiredOptions $groupEntity->getOptions()->filter(function($option) use ($requiredOptionId) {
  358.                         return $requiredOptionId === $option->getId();
  359.                     });
  360.                     if ($requiredOptions->count()) {
  361.                         $prependedOptions->merge($requiredOptions);
  362.                         $requiredOption $requiredOptions->first();
  363.                         if ($requiredOption && $newOptions->count() > &&
  364.                             $requiredOption->getPosition() < $newOptions->first()->getPosition())
  365.                         {
  366.                             // prepend option
  367.                             $requiredOptions->merge($newOptions);
  368.                             $newOptions $requiredOptions;
  369.                             if ($newOptions->count() === $groupConfig->getMaxEntries()) {
  370.                                 $newOptions->remove($newOptions->last()->getId());
  371.                             }
  372.                         } else if ($requiredOption) {
  373.                             // append option
  374.                             if ($newOptions->count() === $groupConfig->getMaxEntries()) {
  375.                                 $newOptions->remove($newOptions->last()->getId());
  376.                             }
  377.                             $newOptions->add($requiredOption);
  378.                         }
  379.                     }
  380.                 }
  381.             }
  382.             $groupEntity->setOptions($newOptions);
  383.         }
  384.         $config->setPrependedOptions($prependedOptions->getKeys());
  385.         return $settings;
  386.     }
  387.     /**
  388.      * Loads product entities for each option.
  389.      */
  390.     protected function loadProductEntities(array $mappings,
  391.         ProductEntity $product,
  392.         SalesChannelContext $context): array
  393.     {
  394.         $variantIds = [];
  395.         foreach ($mappings as $optionId => $mapping) {
  396.             if (isset($mapping['loadEntity']) && $mapping['loadEntity']) {
  397.                 $variantIds[] = $mapping['productId'];
  398.             }
  399.         }
  400.         if ($variantIds) {
  401.             $criteria = new Criteria();
  402.             $criteria->setIds(array_unique($variantIds));
  403.             $criteria->addAssociation('prices');
  404.             /** @var EntityCollection|null $product */
  405.             $products $this->productRepository->search($criteria$context);
  406.             // assign entities to mappings array
  407.             foreach ($mappings as $optionId => $mapping) {
  408.                 if ($products->get($mapping['productId'])) {
  409.                     /** @var SalesChannelProductEntity $variant */
  410.                     $variant $products->get($mapping['productId']);
  411.                     $mappings[$optionId]['entity'] = $variant;
  412.                 }
  413.             }
  414.         }
  415.         return $mappings;
  416.     }
  417.     /**
  418.      * Overrides the default variant preselection in some cases.
  419.      *
  420.      * @param SalesChannelProductEntity[] $products
  421.      */
  422.     protected function handlePreselection(array $products, array $displayConfigsSalesChannelContext $context): void
  423.     {
  424.         $newProductIds $this->resolvePreselectionIds($products$displayConfigs$context);
  425.         if (!$newProductIds) {
  426.             return;
  427.         }
  428.         // load product entity for the new preselection
  429.         $criteria = clone $this->getProductListingCriteria($context);
  430.         $criteria->addAssociation('cover')
  431.             ->addAssociation('options.group')
  432.             ->addAssociation('manufacturer.media')
  433.             ->addAssociation('properties.group');
  434.         $criteria->addFilter(new EqualsAnyFilter('id'array_unique(array_values($newProductIds))));
  435.         $criteria->setLimit(null);
  436.         $criteria->setOffset(null);
  437.         $newProducts $this->productRepository->search($criteria$context);
  438.         // override products with the new ones
  439.         foreach ($products as $productId => $product) {
  440.             if (!isset($newProductIds[$productId])) {
  441.                 continue;
  442.             }
  443.             $newProductId $newProductIds[$productId];
  444.             if (!$newProducts->has($newProductId)) {
  445.                 continue;
  446.             }
  447.             $newProduct = clone $newProducts->get($newProductId);
  448.             foreach ($product->getExtensions() as $name => $extension) {
  449.                 if (!$newProduct->hasExtension($name)) {
  450.                     $newProduct->addExtension($name$extension);
  451.                 }
  452.             }
  453.             foreach ($newProduct->getVars() as $key => $value) {
  454.                 $product->assign([$key => $value]);
  455.             }
  456.             $settings $this->configuratorLoader->load($product$context);
  457.             if ($settings && $settings->count()) {
  458.                 $config $product->getExtension('maxiaListingVariants');
  459.                 $config->setTotalGroupCount($settings->count());
  460.                 $config->setOptions($settings);
  461.                 $config->setOptions($this->filterGroups($product$context));
  462.             }
  463.         }
  464.     }
  465.     /**
  466.      * Returns new product IDs as array (old product ID => new product ID)
  467.      *
  468.      * @param SalesChannelProductEntity[] $products
  469.      */
  470.     protected function resolvePreselectionIds(array $products,
  471.         array $displayConfigs,
  472.         SalesChannelContext $context): array
  473.     {
  474.         $pluginConfig $this->configService->getBaseConfig($context);
  475.         $newProductIds = [];
  476.         foreach ($products as $product) {
  477.             /** @var ProductConfig $productConfig */
  478.             $productConfig $product->getExtension('maxiaListingVariants');
  479.             $settings $productConfig->getOptions();
  480.             $mainVariantId null;
  481.             if ($product->getChildCount() && $pluginConfig->isDisplayParentSupported()) {
  482.                 // parent product without preselection
  483.                 continue;
  484.             }
  485.             $productId $product->getParentId() ?: $product->getId();
  486.             if (!isset($displayConfigs[$productId]) || !$settings || !$settings->count()) {
  487.                 continue;
  488.             }
  489.             $displayConfig $displayConfigs[$productId];
  490.             $mainVariantId $displayConfig['mainVariantId'];
  491.             $displayParent $pluginConfig->isDisplayParentSupported() && $displayConfig['displayParent'];
  492.             $configuratorGroupConfig $displayConfig['configuratorGroupConfig'];
  493.             if ($configuratorGroupConfig && !$mainVariantId && !$displayParent) {
  494.                 // check if expand by property is active (auffaechern), if yes, do not update this product
  495.                 $expandActive false;
  496.                 foreach ($configuratorGroupConfig as $item) {
  497.                     if (isset($item['expressionForListings']) && $item['expressionForListings']) {
  498.                         $expandActive true;
  499.                         break;
  500.                     }
  501.                 }
  502.                 if ($expandActive && !($product->getChildCount() && !$pluginConfig->isDisplayParentSupported())) {
  503.                     continue;
  504.                 }
  505.             }
  506.             if (!$mainVariantId && $pluginConfig->isPreselectFirstVariantByDefault()) {
  507.                 $firstGroup $settings->first();
  508.                 $firstOption $firstGroup && $firstGroup->getOptions()
  509.                     ? $firstGroup->getOptions()->first()
  510.                     : null;
  511.                 if ($firstOption) {
  512.                     try {
  513.                         $foundCombination $this->combinationFinder->find(
  514.                             $displayConfig['parentId'],
  515.                             $firstGroup->getId(),
  516.                             [$firstOption->getId()],
  517.                             !$pluginConfig->isAvoidOutOfStockPreselection(),
  518.                             $context
  519.                         );
  520.                         $mainVariantId $foundCombination->getVariantId();
  521.                         if ($pluginConfig->isAvoidOutOfStockPreselection()) {
  522.                             $displayConfig['available'] = true;
  523.                         }
  524.                     } catch (ProductNotFoundException $e) {}
  525.                 }
  526.             }
  527.             if ($pluginConfig->isAvoidOutOfStockPreselection() && !$displayConfig['available']
  528.                 && $displayConfig['firstAvailableVariantId'])
  529.             {
  530.                 $mainVariantId $displayConfig['firstAvailableVariantId'];
  531.             }
  532.             if (!$mainVariantId && $product->getChildCount() && !$pluginConfig->isDisplayParentSupported()) {
  533.                 $mainVariantId $displayConfig['firstVariantId'];
  534.             }
  535.             if ($mainVariantId && $mainVariantId !== $product->getId()) {
  536.                 $newProductIds[$product->getId()] = $mainVariantId;
  537.             }
  538.         }
  539.         $event = new ResolvePreselectionsEvent($newProductIds$products$displayConfigs$context);
  540.         $this->eventDispatcher->dispatch($event);
  541.         return $event->getNewProductIds();
  542.     }
  543.     /**
  544.      * Loads cover media for all options.
  545.      */
  546.     protected function loadProductCovers(array $mappings,
  547.         ProductEntity $product,
  548.         SalesChannelContext $context): array
  549.     {
  550.         // build product IDs array
  551.         $variantIds = [];
  552.         foreach ($mappings as $mapping) {
  553.             if (!isset($mapping['media'])) {
  554.                 $variantIds[] = $mapping['productId'];
  555.             }
  556.         }
  557.         if (empty($variantIds)) {
  558.             return $mappings;
  559.         }
  560.         $parentId $product->getParentId() ?: $product->getId();
  561.         $variantIds[] = $parentId;
  562.         $productMedia $this->getProductCovers($variantIds$context);
  563.         if (!$productMedia) {
  564.             return $mappings;
  565.         }
  566.         // assign media to options
  567.         foreach ($mappings as $optionId => $mapping) {
  568.             if (isset($productMedia[$mapping['productId']])) {
  569.                 $media $productMedia[$mapping['productId']];
  570.             } else if (isset($productMedia[$parentId])) {
  571.                 $media $productMedia[$parentId];
  572.             } else {
  573.                 continue;
  574.             }
  575.             if ($media) {
  576.                 $mappings[$optionId]['media'] = $media['entity'];
  577.             }
  578.         }
  579.         return $mappings;
  580.     }
  581.     /**
  582.      * Load cover media for multiple products, grouped by product IDs.
  583.      */
  584.     protected function getProductCovers(array $productIdsSalesChannelContext $context): array
  585.     {
  586.         // get media IDs for all products
  587.         $mediaIds $this->dbConnection->fetchAllAssociative("
  588.             SELECT product.id, product_media.media_id FROM product 
  589.             LEFT JOIN product_media ON (product_media.id = product.cover) 
  590.             WHERE product.id IN (:ids) AND product_media.media_id IS NOT NULL
  591.         ",
  592.             ['ids' => Uuid::fromHexToBytesList(array_unique($productIds))],
  593.             ['ids' => Connection::PARAM_STR_ARRAY]
  594.         );
  595.         if (empty($mediaIds)) {
  596.             return [];
  597.         }
  598.         $criteria = new Criteria();
  599.         $criteria->setIds(Uuid::fromBytesToHexList(array_column($mediaIds'media_id')));
  600.         $mediaEntities $this->mediaRepository->search($criteria$context->getContext());
  601.         $productMedia = [];
  602.         foreach ($mediaIds as $item) {
  603.             $productMedia[UUid::fromBytesToHex($item['id'])] =
  604.             $productMedia[UUid::fromBytesToHex($item['id'])] = [
  605.                 'media_id' => UUid::fromBytesToHex($item['media_id']),
  606.                 'entity' => $mediaEntities->get(UUid::fromBytesToHex($item['media_id']))
  607.             ];
  608.         }
  609.         return $productMedia;
  610.     }
  611.     /**
  612.      * Returns cached product listing criteria for the current request.
  613.      */
  614.     public function getProductListingCriteria(SalesChannelContext $context): ?Criteria
  615.     {
  616.         static $criteria;
  617.         if ($criteria === null) {
  618.             $criteria = new Criteria();
  619.             if (class_exists('\Shopware\Core\Content\Product\SalesChannel\Sorting\ProductSortingCollection')) {
  620.                 $criteria->addExtension('sortings'ProductListingCmsElementResolver::createSortings());
  621.             }
  622.             $this->eventDispatcher->dispatch(
  623.                 new ProductListingCriteriaEvent($this->getMainRequest(), $criteria$context)
  624.             );
  625.         }
  626.         return $criteria;
  627.     }
  628.     /**
  629.      * Returns listing criteria for the current configurator selection.
  630.      */
  631.     public function buildCriteria(Request $requestSalesChannelContext $salesChannelContext): Criteria
  632.     {
  633.         $productId $request->query->get('productId');
  634.         $pluginConfig $this->configService->getBaseConfig($salesChannelContext);
  635.         if ($pluginConfig->isDisplayParentSupported()) {
  636.             $displayConfig =  $this->dbConnection->fetchAssociative(
  637.                 'SELECT COALESCE(parent_id, id) as parent_id, display_parent FROM product WHERE id = :id',
  638.                 ['id' => Uuid::fromHexToBytes($productId)]
  639.             );
  640.         } else {
  641.             $displayConfig =  $this->dbConnection->fetchAssociative(
  642.                 'SELECT COALESCE(parent_id, id) as parent_id, 0 as display_parent FROM product WHERE id = :id',
  643.                 ['id' => Uuid::fromHexToBytes($productId)]
  644.             );
  645.         }
  646.         $parentId Uuid::fromBytesToHex($displayConfig['parent_id']);
  647.         if ($request->query->get('options') && $request->query->get('switched')) {
  648.             // find new product id by selected options
  649.             $newOptions json_decode($request->query->get('options'), true);
  650.             $switchedOption $request->query->get('switched');
  651.             try {
  652.                 $foundCombination $this->combinationFinder->find(
  653.                     $parentId$switchedOption$newOptions,
  654.                     true$salesChannelContext
  655.                 );
  656.                 $productId $foundCombination->getVariantId();
  657.             } catch (ProductNotFoundException $e) {}
  658.         }
  659.         $criteria = (new Criteria())
  660.             ->addAssociation('cover')
  661.             ->addAssociation('options.group')
  662.             ->addAssociation('manufacturer.media')
  663.             ->addAssociation('properties.group')
  664.             ->setLimit(1);
  665.         if ($parentId === $productId && $displayConfig['display_parent']) {
  666.             // show parent product (handled by ProductListingLoader::resolvePreviews)
  667.             $criteria->addFilter(new EqualsFilter('product.parentId'$productId));
  668.         } else {
  669.             // show specific variant
  670.             $criteria->addFilter(new EqualsFilter('product.id'$productId));
  671.             // add option filter to prevent ProductListingLoader::resolvePreviews
  672.             $criteria->addFilter(new NotFilter(
  673.                     NotFilter::CONNECTION_AND,
  674.                     [ new EqualsFilter('optionIds''1')]
  675.                 ))
  676.                 ->addPostFilter(new NotFilter(
  677.                     NotFilter::CONNECTION_AND,
  678.                     [ new EqualsFilter('optionIds''1')]
  679.                 ));
  680.         }
  681.         return $criteria;
  682.     }
  683.     protected function getMainRequest(): ?Request
  684.     {
  685.         return method_exists($this->requestStack'getMainRequest')
  686.             ? $this->requestStack->getMainRequest()
  687.             : $this->requestStack->getMasterRequest();
  688.     }
  689. }