vendor/store.shopware.com/syseasimilarproducts/src/Subscriber/ProductSubscriber.php line 78

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace SyseaSimilarProducts\Subscriber;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Category\CategoryEntity;
  5. use Shopware\Core\Content\Product\ProductCollection;
  6. use Shopware\Core\Content\Product\ProductEntity;
  7. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  8. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  9. use Shopware\Core\Framework\Context;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  19. use Shopware\Core\Framework\Uuid\Uuid;
  20. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  21. use Shopware\Core\System\SystemConfig\SystemConfigService;
  22. use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
  23. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  24. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  25. class ProductSubscriber implements EventSubscriberInterface
  26. {
  27.     /**
  28.      * @var SystemConfigService
  29.      */
  30.     private $systemConfigService;
  31.     /**
  32.      * @var EntityRepository
  33.      */
  34.     private $categoryRepository;
  35.     /**
  36.      * @var SalesChannelRepositoryInterface
  37.      */
  38.     private $productRepository;
  39.     /**
  40.      * @var Connection
  41.      */
  42.     private Connection $connection;
  43.     public function __construct(
  44.         EntityRepository $categoryRepository,
  45.         SalesChannelRepositoryInterface $productRepository,
  46.         SystemConfigService $systemConfigService,
  47.         Connection $connection
  48.     )
  49.     {
  50.         $this->categoryRepository $categoryRepository;
  51.         $this->productRepository $productRepository;
  52.         $this->systemConfigService $systemConfigService;
  53.         $this->connection $connection;
  54.     }
  55.     /**
  56.      * @inheritDoc
  57.      */
  58.     public static function getSubscribedEvents()
  59.     {
  60.         return [
  61.             ProductPageCriteriaEvent::class => 'onProductPageCriteria',
  62.             ProductPageLoadedEvent::class => 'onProductPageLoaded'
  63.         ];
  64.     }
  65.     public function onProductPageCriteria(ProductPageCriteriaEvent $event) {
  66.         $event->getCriteria()->addAssociation('categories');
  67.     }
  68.     public function onProductPageLoaded(ProductPageLoadedEvent $event) {
  69.         $page $event->getPage();
  70.         $product $page->getProduct();
  71.         $currentProductId $product->getId();
  72.         $salesChannelContext $event->getSalesChannelContext();
  73.         $categoryIds $this->getProductCategories($product$event->getContext());
  74.         if(!empty($categoryIds)) {
  75.             $similarProducts $this->getSimilarProducts($categoryIds$currentProductId$salesChannelContext$product$salesChannelContext->getSalesChannelId());
  76.             if(!empty($similarProducts)) {
  77.                 $apperanceStyle 'block';
  78.                 $apperance $this->systemConfigService->get('SyseaSimilarProducts.config.apperanceStyle');
  79.                 if($apperance) {
  80.                     $apperanceStyle 'slider';
  81.                 }
  82.                 $similarProductsConfig = [
  83.                     'similarProducts' => $similarProducts,
  84.                     'apperance' => $apperanceStyle
  85.                 ];
  86.                 $similarProductsConfig['boxLayout'] = $this->systemConfigService->get('SyseaSimilarProducts.config.boxLayout');
  87.                 $similarProductsConfig['displayMode'] = $this->systemConfigService->get('SyseaSimilarProducts.config.displayMode');
  88.                 $similarProductsConfig['container'] = $this->systemConfigService->get('SyseaSimilarProducts.config.container');
  89.                 $similarProductsConfig['asTitle'] = $this->systemConfigService->get('SyseaSimilarProducts.config.asTitle');
  90.                 if($apperanceStyle == "slider") {
  91.                     $similarProductsConfig['navigation'] = $this->systemConfigService->get('SyseaSimilarProducts.config.navigation');
  92.                     $similarProductsConfig['elMinWidth'] = $this->systemConfigService->get('SyseaSimilarProducts.config.elMinWidth');
  93.                 }
  94.                 $page->assign([
  95.                     'similarProductsConfig' =>  $similarProductsConfig
  96.                 ]);
  97.             }
  98.         }
  99.     }
  100.     /**
  101.      * @param ProductEntity $product
  102.      * @return array
  103.      */
  104.     private function getProductCategories(ProductEntity $productContext $context): array {
  105.         $categoryIds = [];
  106.         $categoryLevels = [];
  107.         $categories $product->getCategories();
  108.         if(empty($categories->getIds())) {
  109.             return [];
  110.         }
  111.         /**
  112.          * @var CategoryEntity $category
  113.          */
  114.         foreach($categories as $category) {
  115.             if(!in_array($category->getLevel(), $categoryLevels)) {
  116.                 $categoryLevels[] = $category->getLevel();
  117.             }
  118.         }
  119.         $higehstLevel max($categoryLevels);
  120.         //$tolerance = (int)$this->systemConfigService->get('SyseaSimilarProducts.config.categoryLevelTolerance');
  121.         $tolerance 0;
  122.         if($tolerance 0) {
  123.             $tolerance 0;
  124.         }
  125.         foreach($categories as $category) {
  126.             if(!empty($category->getId())) {
  127.                 if($category->getLevel() == $higehstLevel || $category->getLevel() >= ($higehstLevel $tolerance)) {
  128.                     if($category->getActive()) {
  129.                         $categoryIds[] = $category->getId();
  130.                     }
  131.                 }
  132.             }
  133.             if($this->systemConfigService->get('SyseaSimilarProducts.config.includeParentCategory')) {
  134.                 if(!empty($category->getParentId())) {
  135.                     $criteria = new Criteria();
  136.                     $criteria->addFilter(new EqualsFilter('id'$category->getParentId()));
  137.                     /** @var CategoryEntity $parentCategory */
  138.                     $parentCategory $this->categoryRepository->search($criteria$context)->getEntities()->first();
  139.                     if($parentCategory->getActive() && $parentCategory->getLevel() !== 1) {
  140.                         $categoryIds[] = $category->getParentId();
  141.                     }
  142.                 }
  143.             }
  144.         }
  145.         return $categoryIds;
  146.     }
  147.     private function getSimilarProducts($categoryIds$currentProductId$context$pageProduct$salesChannelId): ProductCollection {
  148.         if($this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
  149.             shuffle($categoryIds);
  150.         }
  151.         $similarMax = (int)$this->systemConfigService->get('SyseaSimilarProducts.config.similarProductsLimit');
  152.         if($similarMax 20) {
  153.             $similarMax 20// capped to 20 items to prevent memory limit exceed
  154.         }
  155.         $criteria = new Criteria();
  156.         $criteria->addAssociation('categories');
  157.         $criteria->addAssociations([
  158.             'categories''cover''manufacturer''prices'
  159.         ]);
  160.         $criteria->addFilter(new NotFilter(
  161.             NotFilter::CONNECTION_AND,
  162.             [
  163.                 new EqualsFilter('id'$currentProductId)
  164.             ]
  165.         ));
  166.         $criteria->addFilter(new EqualsAnyFilter('categoryTree'$categoryIds));
  167.         if(!$this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
  168.             $criteria->setLimit($similarMax);
  169.         } else {
  170.             $criteria->setLimit(20);
  171.         }
  172.         if($criteria->getLimit() > 20 || $criteria->getLimit() === null) {
  173.             $criteria->setLimit(20);
  174.         }
  175.         if($this->systemConfigService->get('SyseaSimilarProducts.config.properties')) {
  176.             $propertyFilter $this->getPropertyFilter($pageProduct$salesChannelId);
  177.             $filter $propertyFilter->getFilter();
  178.             $criteria->addFilter($filter);
  179.         }
  180.         // only show active products
  181.         $criteria->addFilter(new EqualsFilter('active'true));
  182.         // hide products with stock <= 0 if active
  183.         if($this->systemConfigService->get('SyseaSimilarProducts.config.hideSoldoutProducts')) {
  184.             $criteria->addFilter(
  185.                 new RangeFilter('stock', [
  186.                     RangeFilter::GT => 0
  187.                 ])
  188.             );
  189.         }
  190.         // only show parent product if active
  191.         if($this->systemConfigService->get('SyseaSimilarProducts.config.onlyMainVariant')) {
  192.             $criteria->addFilter(new EqualsFilter('parentId'null));
  193.         }
  194.         $productSearchResult $this->productRepository->search($criteria$context);
  195.         $products $productSearchResult->getEntities();
  196.         $productIds = [];
  197.         if(!$this->systemConfigService->get('SyseaSimilarProducts.config.onlyMainVariant')) {
  198.             if(!$this->systemConfigService->get('SyseaSimilarProducts.config.useStorefrontRepresentation')) {
  199.                 $productIds $products->getIds();
  200.             } else {
  201.                 $propertyGroupIds = [];
  202.                 /**
  203.                  * @var SalesChannelProductEntity $product
  204.                  */
  205.                 foreach($products as $product) {
  206.                     if(!is_null($product->getVariantListingConfig()))  {
  207.                         $configuratorGroupConfig $product->getVariantListingConfig();
  208.                         if($configuratorGroupConfig->getDisplayParent()) {
  209.                             if(!is_null($product->getParentId())) {
  210.                                 $productIds[] = $product->getParentId();
  211.                             } else {
  212.                                 $productIds[] = $product->getId();
  213.                             }
  214.                             continue;
  215.                         }
  216.                         if(!$configuratorGroupConfig->getDisplayParent() && $configuratorGroupConfig->getMainVariantId()) {
  217.                             $productIds[] = $configuratorGroupConfig->getMainVariantId();
  218.                             continue;
  219.                         }
  220.                         foreach($configuratorGroupConfig->getConfiguratorGroupConfig() as $groupConfig) {
  221.                             if($groupConfig['expressionForListings']) {
  222.                                 if(!in_array($groupConfig['id'], $propertyGroupIds)) {
  223.                                     $propertyGroupIds[] = $groupConfig['id'];
  224.                                 }
  225.                                 $productIds[] = $product->getId();
  226.                             } else {
  227.                                 $productIds[] = $product->getId();
  228.                             }
  229.                         }
  230.                     } else {
  231.                         $productIds[] = $product->getId();
  232.                     }
  233.                 }
  234.             }
  235.         } else {
  236.             $productIds $products->getIds();
  237.         }
  238.         $criteria->addFilter(new EqualsAnyFilter('id'$productIds));
  239.         $productSearchResult $this->productRepository->search($criteria$context);
  240.         if($this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
  241.             $i 0;
  242.             $randomProductCollection = new ProductCollection();
  243.             /**
  244.              * @var ProductCollection $productCollection
  245.              */
  246.             $productCollection $productSearchResult->getEntities();
  247.             $productElements $productCollection->getElements();
  248.             shuffle($productElements);
  249.             foreach($productElements as $productElement) {
  250.                 $randomProductCollection->add($productElement);
  251.                 $i++;
  252.                 if($i == $similarMax)  {
  253.                     break;
  254.                 }
  255.             }
  256.             return $randomProductCollection;
  257.         }
  258.         /**
  259.          * @var ProductCollection $similarProducts
  260.          */
  261.         $similarProducts $productSearchResult->getEntities();
  262.         return $similarProducts;
  263.     }
  264.     private function getPropertyFilter(SalesChannelProductEntity $product$salesChannelId)
  265.     {
  266.         $properties $product->getProperties();
  267.         $options $product->getOptions();
  268.         $filterProperties $this->systemConfigService->get('SyseaSimilarProducts.config.properties'$salesChannelId) ?: [];
  269.         $ids = [];
  270.         $filters = [];
  271.         foreach ($properties as $property) {
  272.             if (in_array($property->getGroupId(), $filterPropertiestrue)) {
  273.                 $ids[] = $property->getId();
  274.             }
  275.         }
  276.         foreach ($options as $option) {
  277.             if (in_array($option->getGroupId(), $filterPropertiestrue)) {
  278.                 $ids[] = $option->getId();
  279.             }
  280.         }
  281.         $grouped $this->connection->fetchAllAssociative("
  282.                 SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id 
  283.                 FROM property_group_option WHERE id IN (:ids)
  284.             ",
  285.             ['ids' => Uuid::fromHexToBytesList($ids)],
  286.             ['ids' => Connection::PARAM_STR_ARRAY]
  287.         );
  288.         $grouped FetchModeHelper::group($grouped);
  289.         foreach ($grouped as $options) {
  290.             $options array_column($options'id');
  291.             $filters[] = new MultiFilter(
  292.                 MultiFilter::CONNECTION_OR,
  293.                 [
  294.                     new EqualsAnyFilter('product.optionIds'$options),
  295.                     new EqualsAnyFilter('product.propertyIds'$options),
  296.                 ]
  297.             );
  298.         }
  299.         $configOperator $this->systemConfigService->get('SyseaSimilarProducts.config.propertyLogic'$salesChannelId) ?: 'or';
  300.         $operator MultiFilter::CONNECTION_OR;
  301.         if (strtolower($configOperator) === 'and') {
  302.             $operator MultiFilter::CONNECTION_AND;
  303.         }
  304.         $optionAggregation = new TermsAggregation('options''product.options.id');
  305.         $propertyAggregation = new TermsAggregation('properties''product.properties.id');
  306.         return new Filter(
  307.             'properties',
  308.             true,
  309.             [
  310.                 $propertyAggregation,
  311.                 $optionAggregation
  312.             ],
  313.             new MultiFilter($operator$filters),
  314.             $ids,
  315.             false
  316.         );
  317.     }
  318. }