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

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