vendor/store.shopware.com/cogipropertycrossselling/src/Subscriber/ProductPageSubscriber.php line 181

Open in your IDE?
  1. <?php
  2. namespace Cogi\PropertyCrossSelling\Subscriber;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Cms\Events\CmsPageLoadedEvent;
  5. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
  6. use Shopware\Core\Content\Product\Events\ProductCrossSellingsLoadedEvent;
  7. use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElement;
  8. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  9. use Shopware\Core\Content\Product\SalesChannel\ProductCloseoutFilter;
  10. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  20. use Shopware\Core\Framework\Uuid\Uuid;
  21. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  22. use Shopware\Core\System\SystemConfig\SystemConfigService;
  23. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  24. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  25. use Shopware\Core\Framework\Adapter\Translation\Translator;
  26. use Symfony\Component\HttpFoundation\Request;
  27. class ProductPageSubscriber implements EventSubscriberInterface
  28. {
  29.     /**
  30.      * @var SystemConfigService
  31.      */
  32.     private $systemConfigService;
  33.     /**
  34.      * @var SalesChannelRepositoryInterface
  35.      */
  36.     private $salesChannelProductRepository;
  37.     /**
  38.      * @var Connection
  39.      */
  40.     private $connection;
  41.     /**
  42.      * @var Translator
  43.      */
  44.     private $translator;
  45.     public function __construct(
  46.         Connection $connection,
  47.         SalesChannelRepositoryInterface $salesChannelProductRepository,
  48.         SystemConfigService $systemConfigService,
  49.         Translator $translator
  50.     )
  51.     {
  52.         $this->connection $connection;
  53.         $this->salesChannelProductRepository $salesChannelProductRepository;
  54.         $this->systemConfigService $systemConfigService;
  55.         $this->translator $translator;
  56.     }
  57.     public static function getSubscribedEvents(): array
  58.     {
  59.         return [
  60.             ProductPageLoadedEvent::class => 'onPageLoaded',
  61. //            ProductCrossSellingsLoadedEvent::class => 'onCmsCrossSellingsLoaded'
  62.         ];
  63.     }
  64.     /**
  65.      * The function below IS and WONT work in near future because the event does not delivery any product ID if the
  66.      * cross sellings are empty
  67.      *
  68.      * @param ProductCrossSellingsLoadedEvent $event
  69.      * @param string $request
  70.      * @param $data
  71.      * @return void
  72.      */
  73.     public function onCmsCrossSellingsLoaded(ProductCrossSellingsLoadedEvent $eventstring $request$data): void
  74.     {
  75.         $salesChannelId $event->getSalesChannelContext()->getSalesChannel()->getId();
  76.         $autoAdd false;
  77.         $insert 'after';
  78.         if (!count($event->getCrossSellings()) && $this->systemConfigService->get('CogiPropertyCrossSelling.config.useAsDefault'$salesChannelId)) {
  79.             $autoAdd true;
  80.         }
  81.         if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.useAlways'$salesChannelId)) {
  82.             $autoAdd true;
  83.             $insert $this->systemConfigService->get('CogiPropertyCrossSelling.config.useAlwaysPosition'$salesChannelId) ?: 'after';
  84.         }
  85.         // Use property cross selling as default if no other cross selling defined
  86.         if ($autoAdd) {
  87.             $crossSellings $event->getCrossSellings();
  88.             $crossSellingElement = new CrossSellingElement();
  89.             $crossSelling = new ProductCrossSellingEntity();
  90.             $defaultName $this->translator->trans('cogiPropertyCrossSelling.defaultCrossSellingName');
  91.             $crossSelling->setName($defaultName);
  92.             $crossSelling->setTranslated(['name' => $defaultName]);
  93.             $crossSelling->setType('cogiPropertyCrossSelling');
  94.             $crossSelling->setId(md5('crossSelling' rand()));
  95.             $crossSelling->setActive(true);
  96.             $crossSellingElement->setCrossSelling($crossSelling);
  97.             $crossSellingElement->setTotal(1);
  98.             if ($insert === 'before') {
  99.                 $elements $crossSellings->getElements();
  100.                 $crossSellings->clear();
  101.                 $crossSellings->add($crossSellingElement);
  102.                 foreach ($elements as $element) {
  103.                     $crossSellings->add($element);
  104.                 }
  105.             } else {
  106.                 $crossSellings->add($crossSellingElement);
  107.             }
  108.         }
  109.         foreach ($event->getCrossSellings() as $crossSelling) {
  110.             if ($crossSelling->getCrossSelling()->getType() === 'cogiPropertyCrossSelling') {
  111.                 $useManufacturer $this->systemConfigService->get('CogiPropertyCrossSelling.config.useManufacturer'$salesChannelId);
  112.                 $manufacturerMustMatch $this->systemConfigService->get('CogiPropertyCrossSelling.config.manufacturerMustMatch'$salesChannelId);
  113.                 // Get the property filter
  114.                 $propertyFilter $this->getPropertyFilter($product$salesChannelId);
  115.                 $filter $propertyFilter->getFilter();
  116.                 // If manufacturer should be used and is set for product, get filter and set connection accordingly
  117.                 if ($useManufacturer && ($product->getManufacturerId() || $manufacturerMustMatch)) {
  118.                     $manufacturerFilter $this->getManufacturerFilter($product);
  119.                     $filterConnection $manufacturerMustMatch MultiFilter::CONNECTION_AND MultiFilter::CONNECTION_OR;
  120.                     $filter = new MultiFilter($filterConnection, [
  121.                         $manufacturerFilter->getFilter(),
  122.                         $propertyFilter->getFilter()
  123.                     ]);
  124.                 }
  125.                 // Configure Criteria
  126.                 $criteria = new Criteria();
  127.                 $criteria->setLimit($this->systemConfigService->get('CogiPropertyCrossSelling.config.limit'$salesChannelId) ?: 15);
  128.                 $criteria->addFilter($filter);
  129.                 // Make sure current product is not shown in cross selling
  130.                 $criteria->addFilter(new NotFilter(
  131.                     NotFilter::CONNECTION_AND,
  132.                     [new EqualsFilter('id'$product->getId())]
  133.                 ));
  134.                 // Exclude variants of the same product
  135.                 if ($product->getParentId()) {
  136.                     $criteria->addFilter(new NotFilter(
  137.                         NotFilter::CONNECTION_AND,
  138.                         [new EqualsFilter('parentId'$product->getParentId())]
  139.                     ));
  140.                     $criteria->addFilter(new NotFilter(
  141.                         NotFilter::CONNECTION_AND,
  142.                         [new EqualsFilter('id'$product->getParentId())]
  143.                     ));
  144.                 }
  145.                 // Exclude unavailable products if set in config
  146.                 if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.excludeUnavailable'$salesChannelId)) {
  147.                     $criteria->addFilter(new ProductCloseoutFilter());
  148.                 }
  149.                 // Get products and set for cross selling
  150.                 $result $this->salesChannelProductRepository->search($criteria$event->getSalesChannelContext());
  151.                 $products $result->getEntities();
  152.                 $crossSelling->setProducts($products);
  153.             }
  154.         }
  155.     }
  156.     public function onPageLoaded(ProductPageLoadedEvent $event): void
  157.     {
  158.         $page $event->getPage();
  159.         // For CMS product page cross sellings are loaded via DataResolver
  160.         if ($page->getCmsPage()) {
  161.             return;
  162.         }
  163.         $product $page->getProduct();
  164.         $salesChannelId $event->getSalesChannelContext()->getSalesChannel()->getId();
  165.         $autoAdd false;
  166.         $insert 'after';
  167.         if (!count($page->getCrossSellings()) && $this->systemConfigService->get('CogiPropertyCrossSelling.config.useAsDefault'$salesChannelId)) {
  168.             $autoAdd true;
  169.         }
  170.         if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.useAlways'$salesChannelId)) {
  171.             $autoAdd true;
  172.             $insert $this->systemConfigService->get('CogiPropertyCrossSelling.config.useAlwaysPosition'$salesChannelId) ?: 'after';
  173.         }
  174.         // Use property cross selling as default if no other cross selling defined
  175.         if ($autoAdd) {
  176.             $crossSellings $page->getCrossSellings();
  177.             $crossSellingElement = new CrossSellingElement();
  178.             $crossSelling = new ProductCrossSellingEntity();
  179.             $defaultName $this->translator->trans('cogiPropertyCrossSelling.defaultCrossSellingName');
  180.             $crossSelling->setName($defaultName);
  181.             $crossSelling->setTranslated(['name' => $defaultName]);
  182.             $crossSelling->setType('cogiPropertyCrossSelling');
  183.             $crossSelling->setId(md5('crossSelling' $product->getId()));
  184.             $crossSelling->setActive(true);
  185.             $crossSellingElement->setCrossSelling($crossSelling);
  186.             $crossSellingElement->setTotal(1);
  187.             if ($insert === 'before') {
  188.                 $elements $crossSellings->getElements();
  189.                 $crossSellings->clear();
  190.                 $crossSellings->add($crossSellingElement);
  191.                 foreach ($elements as $element) {
  192.                     $crossSellings->add($element);
  193.                 }
  194.             } else {
  195.                 $crossSellings->add($crossSellingElement);
  196.             }
  197.         }
  198.         foreach ($page->getCrossSellings() as $crossSelling) {
  199.             if ($crossSelling->getCrossSelling()->getType() === 'cogiPropertyCrossSelling') {
  200.                 $useManufacturer $this->systemConfigService->get('CogiPropertyCrossSelling.config.useManufacturer'$salesChannelId);
  201.                 $manufacturerMustMatch $this->systemConfigService->get('CogiPropertyCrossSelling.config.manufacturerMustMatch'$salesChannelId);
  202.                 // Get the property filter
  203.                 $propertyFilter $this->getPropertyFilter($product$salesChannelId);
  204.                 $filter $propertyFilter->getFilter();
  205.                 // If manufacturer should be used and is set for product, get filter and set connection accordingly
  206.                 if ($useManufacturer && ($product->getManufacturerId() || $manufacturerMustMatch)) {
  207.                     $manufacturerFilter $this->getManufacturerFilter($product);
  208.                     $filterConnection $manufacturerMustMatch MultiFilter::CONNECTION_AND MultiFilter::CONNECTION_OR;
  209.                     $filter = new MultiFilter($filterConnection, [
  210.                         $manufacturerFilter->getFilter(),
  211.                         $propertyFilter->getFilter()
  212.                     ]);
  213.                 }
  214.                 // Configure Criteria
  215.                 $criteria = new Criteria();
  216.                 $criteria->setLimit($this->systemConfigService->get('CogiPropertyCrossSelling.config.limit'$salesChannelId) ?: 15);
  217.                 $criteria->addFilter($filter);
  218.                 // Make sure current product is not shown in cross selling
  219.                 $criteria->addFilter(new NotFilter(
  220.                     NotFilter::CONNECTION_AND,
  221.                     [new EqualsFilter('id'$product->getId())]
  222.                 ));
  223.                 // Exclude variants of the same product
  224.                 if ($product->getParentId()) {
  225.                     $criteria->addFilter(new NotFilter(
  226.                         NotFilter::CONNECTION_AND,
  227.                         [new EqualsFilter('parentId'$product->getParentId())]
  228.                     ));
  229.                     $criteria->addFilter(new NotFilter(
  230.                         NotFilter::CONNECTION_AND,
  231.                         [new EqualsFilter('id'$product->getParentId())]
  232.                     ));
  233.                 }
  234.                 // Exclude unavailable products if set in config
  235.                 if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.excludeUnavailable'$salesChannelId)) {
  236.                     $criteria->addFilter(new ProductCloseoutFilter());
  237.                 }
  238.                 // Exclude products from the
  239.                 if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.onlyProductsFromSameCategory'$salesChannelId)) {
  240.                     $categoryFilterList = [];
  241.                     // add a contains filter for each category Id
  242.                     foreach ($product->getCategoryIds() as $categoryId){
  243.                         $categoryFilterList[] = new ContainsFilter("categoryIds"$categoryId);
  244.                     }
  245.                     $criteria->addFilter(new MultiFilter(
  246.                          NotFilter::CONNECTION_OR,
  247.                         $categoryFilterList
  248.                     ));
  249.                 }
  250. //                dd($product->getCategoryIds());
  251. //                 dd($criteria);
  252.                 // Get products and set for cross selling
  253.                 $result $this->salesChannelProductRepository->search($criteria$event->getSalesChannelContext());
  254.                 $products $result->getEntities();
  255.                 $crossSelling->setProducts($products);
  256.             }
  257.         }
  258.     }
  259.     protected function getManufacturerFilter(SalesChannelProductEntity $product)
  260.     {
  261.         // If current product has no manufacturer, use NULL for filter
  262.         $filter = new EqualsFilter('product.manufacturerId'null);
  263.         if (!empty($product->getManufacturerId())) {
  264.             $filter = new EqualsAnyFilter('product.manufacturerId', [$product->getManufacturerId()]);
  265.         }
  266.         return new Filter(
  267.             'manufacturer',
  268.             !empty($product->getManufacturerId()),
  269.             [new EntityAggregation('manufacturer''product.manufacturerId''product_manufacturer')],
  270.             $filter,
  271.             [$product->getManufacturerId()]
  272.         );
  273.     }
  274.     protected function getPropertyFilter(SalesChannelProductEntity $product$salesChannelId)
  275.     {
  276.         // Get property options to match against
  277.         $productProperties $product->getProperties();
  278.         $productOptions $product->getOptions();
  279.         $configProperties $this->systemConfigService->get('CogiPropertyCrossSelling.config.properties'$salesChannelId);
  280.         $ids = [];
  281.         // Gather properties
  282.         foreach ($productProperties as $productProperty) {
  283.             if (in_array($productProperty->getGroupId(), $configPropertiestrue)) {
  284.                 $ids[] = $productProperty->getId();
  285.             }
  286.         }
  287.         // Gather options
  288.         foreach ($productOptions as $productOption) {
  289.             if (in_array($productOption->getGroupId(), $configPropertiestrue)) {
  290.                 $ids[] = $productOption->getId();
  291.             }
  292.         }
  293.         // Set the filter
  294.         $propertyAggregation = new TermsAggregation('properties''product.properties.id');
  295.         $optionAggregation = new TermsAggregation('options''product.options.id');
  296.         $grouped $this->connection->fetchAllAssociative(
  297.             'SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id FROM property_group_option WHERE id IN (:ids)',
  298.             ['ids' => Uuid::fromHexToBytesList($ids)],
  299.             ['ids' => Connection::PARAM_STR_ARRAY]
  300.         );
  301.         $grouped FetchModeHelper::group($grouped);
  302.         $filters = [];
  303.         foreach ($grouped as $options) {
  304.             $options array_column($options'id');
  305.             $filters[] = new MultiFilter(
  306.                 MultiFilter::CONNECTION_OR,
  307.                 [
  308.                     new EqualsAnyFilter('product.optionIds'$options),
  309.                     new EqualsAnyFilter('product.propertyIds'$options),
  310.                 ]
  311.             );
  312.         }
  313.         $op $this->systemConfigService->get('CogiPropertyCrossSelling.config.propertyLogic'$salesChannelId) ?: 'or';
  314.         $operator MultiFilter::CONNECTION_OR;
  315.         if (strtolower($op) === 'and') {
  316.             $operator MultiFilter::CONNECTION_AND;
  317.         }
  318.         return new Filter(
  319.             'properties',
  320.             true,
  321.             [$propertyAggregation$optionAggregation],
  322.             new MultiFilter($operator$filters),
  323.             $ids,
  324.             false
  325.         );
  326.     }
  327. }