custom/plugins/MkxBetterVariants/src/Subscriber/Storefront.php line 62

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace MkxBetterVariants\Subscriber;
  3. use MkxBetterVariants\Enumeration\CustomField as Enum;
  4. use Shopware\Core\Content\Cms\Events\CmsPageLoadedEvent;
  5. use Shopware\Core\Content\Product\Events\ProductCrossSellingCriteriaEvent;
  6. use Shopware\Core\Content\Product\Events\ProductCrossSellingIdsCriteriaEvent;
  7. use Shopware\Core\Content\Product\Events\ProductCrossSellingsLoadedEvent;
  8. use Shopware\Core\Content\Product\Events\ProductCrossSellingStreamCriteriaEvent;
  9. use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
  10. use Shopware\Core\Content\Product\Events\ProductSearchResultEvent;
  11. use Shopware\Core\Content\Product\Events\ProductSuggestResultEvent;
  12. use Shopware\Core\Content\Product\ProductCollection;
  13. use Shopware\Core\Content\Product\ProductEntity;
  14. use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElement;
  15. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  17. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  18. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  19. use Shopware\Core\System\SystemConfig\SystemConfigService;
  20. use Shopware\Storefront\Page\LandingPage\LandingPageLoadedEvent;
  21. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  22. use Symfony\Component\DependencyInjection\ContainerInterface;
  23. class Storefront implements EventSubscriberInterface
  24. {
  25.     private const LIMIT_VARIANTS_ONE_VARIATION 30;
  26.     private const LIMIT_VARIANTS_MORE_VARIATIONS 20;
  27.     /** @var ContainerInterface $container */
  28.     private $container;
  29.     /** @var SalesChannelRepositoryInterface $salesChannelProductRepository */
  30.     private $salesChannelProductRepository;
  31.     public function __construct(
  32.         ContainerInterface $container,
  33.         SalesChannelRepositoryInterface $salesChannelProductRepository
  34.     )
  35.     {
  36.         $this->container $container;
  37.         $this->salesChannelProductRepository $salesChannelProductRepository;
  38.     }
  39.     public static function getSubscribedEvents(): array
  40.     {
  41.         return [
  42.             ProductListingResultEvent::class => 'enrichVariantProductsForResult',
  43.             ProductSearchResultEvent::class => 'enrichVariantProductsForResult',
  44.             ProductSuggestResultEvent::class => 'enrichVariantProductsForResult',
  45.             ProductCrossSellingsLoadedEvent::class => 'enrichVariantProductsForCrossSelling',
  46.             ProductCrossSellingIdsCriteriaEvent::class => 'enrichCrossSellingCriteria',
  47.             ProductCrossSellingStreamCriteriaEvent::class => 'enrichCrossSellingCriteria',
  48.         ];
  49.     }
  50.     /**
  51.      * @param ProductCrossSellingCriteriaEvent $event
  52.      * @return void
  53.      */
  54.     public function enrichCrossSellingCriteria(ProductCrossSellingCriteriaEvent $event) {
  55.         $criteria $event->getCriteria();
  56.         $criteria->addAssociation('options');
  57.         $criteria->addAssociation('options.group');
  58.     }
  59.     /**
  60.      * @param ProductListingResultEvent|ProductSearchResultEvent|ProductSuggestResultEvent $event
  61.      * @return void
  62.      */
  63.     public function enrichVariantProductsForResult($event): void
  64.     {
  65.         $products $event->getResult()->getElements();
  66.         $this->enrichVariantProducts($products$event->getSalesChannelContext());
  67.     }
  68.     /**
  69.      * @param ProductCrossSellingsLoadedEvent $event
  70.      * @return void
  71.      */
  72.     public function enrichVariantProductsForCrossSelling(ProductCrossSellingsLoadedEvent $event): void
  73.     {
  74.         $crossSellings $event->getCrossSellings();
  75.         $products = [];
  76.         /** @var CrossSellingElement $crossSelling */
  77.         foreach ($crossSellings as $crossSelling) {
  78.             $products array_merge($products$crossSelling->getProducts()->filter(function ($product) {
  79.                 return
  80.                     isset($product->getCustomFields()[Enum::SHOW_IN_CROSS_SELLING])
  81.                     && $product->getCustomFields()[Enum::SHOW_IN_CROSS_SELLING] === true;
  82.             })->getElements());
  83.         }
  84.         $this->enrichVariantProducts($products$event->getSalesChannelContext());
  85.     }
  86.     /**
  87.      * @param ProductEntity[] $products
  88.      * @param SalesChannelContext $context
  89.      */
  90.     private function enrichVariantProducts($products$context): void
  91.     {
  92.         $parentIds = [];
  93.         $parentIdsForChildrenQuery = [];
  94.         foreach ($products as $product) {
  95.             if (
  96.                 $product->getParentId() !== null
  97.                 && $this->canUseParentData($product)
  98.             ) {
  99.                 $parentIds[$product->getId()] = $product->getParentId();
  100.                 if (!$this->canHideVariants($product)) {
  101.                     $parentIdsForChildrenQuery[] = $product->getParentId();
  102.                 }
  103.             }
  104.         }
  105.         if (count($parentIds) == 0) {
  106.             return;
  107.         }
  108.         // Add parent data to the products
  109.         $criteria = new Criteria($parentIds);
  110.         $productParents $this->salesChannelProductRepository->search($criteria$context);
  111.         /** @var SalesChannelProductEntity $product */
  112.         foreach ($products as $product) {
  113.             $parentId $product->getParentId();
  114.             if (
  115.                 $parentId !== null
  116.                 && $product->getMainVariantId() !== null
  117.                 && $this->canUseParentData($product)
  118.             ) {
  119.                 $parent $productParents->getEntities()->get($parentId);
  120.                 if ($parent instanceof SalesChannelProductEntity) {
  121.                     $product->mkxBVParent $parent;
  122.                     $product->mkxBVVariants null// If it is not changed later on, the variants will not be shown
  123.                 }
  124.             }
  125.         }
  126.         // This is kept separate because the parent objects are significantly larger in this case
  127.         foreach ($parentIdsForChildrenQuery as $id) {
  128.             // Split the queries into 1-id batches to prevent too high memory usage with multiple variant-rich products
  129.             $criteria = new Criteria([$id]);
  130.             $criteria
  131.                 ->addAssociation('children')
  132.                 ->addAssociation('children.options')
  133.                 ->addAssociation('children.options.group')
  134.             ;
  135.             $parentResult $this->salesChannelProductRepository->search($criteria$context);
  136.             /** @var SalesChannelProductEntity $product */
  137.             foreach ($products as $product) {
  138.                 $parentId $product->getParentId();
  139.                 if (
  140.                     $parentId !== null
  141.                     && $parentId === $id
  142.                     && $product->getMainVariantId() !== null
  143.                     && $this->canUseParentData($product)
  144.                     && !$this->canHideVariants($product)
  145.                 ) {
  146.                     $parent $parentResult->first();
  147.                     if ($parent instanceof SalesChannelProductEntity) {
  148.                         if ($this->canShowGroups($product)) {
  149.                             $product->mkxBVGroups $this->getGroupsForProduct($parent$this->canShowGroupNumbers($product));
  150.                         } else {
  151.                             $variants $this->getVariantsForProduct($parent);
  152.                             // Check how many variations in an example product
  153.                             $firstItem $variants[array_key_first($variants)];
  154.                             $numberOfVariations count($firstItem['variations']);
  155.                             if ($numberOfVariations === 1) {
  156.                                 $limit self::LIMIT_VARIANTS_ONE_VARIATION;
  157.                             } else {
  158.                                 $limit self::LIMIT_VARIANTS_MORE_VARIATIONS;
  159.                             }
  160.                             $product->mkxBVVariants array_slice($variants0$limit);
  161.                         }
  162.                     }
  163.                 }
  164.             }
  165.         }
  166.     }
  167.     private function getGroupsForProduct(ProductEntity $parentbool $canShowNumbers): array
  168.     {
  169.         $children $parent->getChildren();
  170.         $groupOptions = [];
  171.         if ($children instanceof ProductCollection) {
  172.             /** @var ProductEntity $child */
  173.             foreach ($children as $child) {
  174.                 if (!$child->getActive()) {
  175.                     continue;
  176.                 }
  177.                 $variation $child->getVariation();
  178.                 foreach ($variation as $singleOptionPair) {
  179.                     $group $singleOptionPair['group'];
  180.                     $option $singleOptionPair['option'];
  181.                     $groupOptions[$group][] = $option;
  182.                 }
  183.             }
  184.         }
  185.         $groupInfo = [];
  186.         $groupOptions array_map('array_unique'$groupOptions);
  187.         foreach ($groupOptions as $group => $options) {
  188.             $newItem = [
  189.                 'group' => $group,
  190.             ];
  191.             if ($canShowNumbers) {
  192.                 $newItem['count'] = count($options);
  193.             }
  194.             $groupInfo[] = $newItem;
  195.         }
  196.         usort($groupInfo, function ($item1$item2) {
  197.             return $item2['count'] <=> $item1['count'];
  198.         });
  199.         return $groupInfo;
  200.     }
  201.     private function getVariantsForProduct(ProductEntity $parent): array
  202.     {
  203.         $children $parent->getChildren();
  204.         $variantArray = [];
  205.         if ($children instanceof ProductCollection) {
  206.             /** @var ProductEntity $child */
  207.             foreach ($children as $child) {
  208.                 if (!$this->canShowProduct($child)) {
  209.                     continue;
  210.                 }
  211.                 $variation $child->getVariation();
  212.                 $variantName implode(' 'array_column($variation'option'));
  213.                 $variantArray[$variantName] = [
  214.                     'productId' => $child->getId(),
  215.                     'variations' => $variation,
  216.                     'position' => count($variation) === $child->getOptions()->first()->getPosition() : 0,
  217.                 ];
  218.             }
  219.             ksort($variantArraySORT_NATURAL);
  220.             uasort($variantArray, function ($item1$item2) {
  221.                 return $item1['position'] <=> $item2['position'];
  222.             });
  223.         }
  224.         return $variantArray;
  225.     }
  226.     private function canShowProduct(ProductEntity $product) {
  227.         if (!$product->getActive()) {
  228.             return false;
  229.         }
  230.         if (
  231.             $this->container->get(SystemConfigService::class)->get('core.listing.hideCloseoutProductsWhenOutOfStock') === true
  232.             && $product->getIsCloseout()
  233.             && $product->getStock() === 0
  234.         ) {
  235.             return false;
  236.         }
  237.         return true;
  238.     }
  239.     private function canUseParentData(ProductEntity $product): bool
  240.     {
  241.         return $this->isCustomFieldTrue($productEnum::USE_PARENT_DATA);
  242.     }
  243.     private function canHideVariants(ProductEntity $product): bool
  244.     {
  245.         return $this->isCustomFieldTrue($productEnum::HIDE_VARIANTS);
  246.     }
  247.     private function canShowGroups(ProductEntity $product): bool
  248.     {
  249.         return $this->isCustomFieldTrue($productEnum::SHOW_GROUPS);
  250.     }
  251.     private function canShowGroupNumbers(ProductEntity $product): bool
  252.     {
  253.         return $this->isCustomFieldTrue($productEnum::SHOW_GROUP_NUMBERS);
  254.     }
  255.     private function isCustomFieldTrue(ProductEntity $productstring $fieldName): bool
  256.     {
  257.         $customFields $product->getCustomFields();
  258.         return $customFields[$fieldName] ?? false;
  259.     }
  260. }