vendor/store.shopware.com/cogipropertycrossselling/src/Subscriber/CheckoutFinishPageSubscriber.php line 83

Open in your IDE?
  1. <?php
  2. namespace Cogi\PropertyCrossSelling\Subscriber;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
  5. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection;
  6. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
  7. use Shopware\Core\Content\Product\ProductCollection;
  8. use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElement;
  9. use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElementCollection;
  10. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  11. use Shopware\Core\Content\Product\SalesChannel\ProductCloseoutFilter;
  12. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\CriteriaQueryBuilder;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  15. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  23. use Shopware\Core\Framework\Uuid\Uuid;
  24. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedEvent;
  25. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  26. use Shopware\Core\System\SystemConfig\SystemConfigService;
  27. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  28. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  29. use Shopware\Core\Framework\Adapter\Translation\Translator;
  30. class CheckoutFinishPageSubscriber implements EventSubscriberInterface
  31. {
  32.     /**
  33.      * @var SystemConfigService
  34.      */
  35.     private $systemConfigService;
  36.     /**
  37.      * @var SalesChannelRepositoryInterface
  38.      */
  39.     private $salesChannelProductRepository;
  40.     /**
  41.      * @var Connection
  42.      */
  43.     private $connection;
  44.     /**
  45.      * @var Translator
  46.      */
  47.     private $translator;
  48.     /**
  49.      * @var EntityRepositoryInterface
  50.      */
  51.     private $propertyGroupOptionRepository;
  52.     public function __construct(
  53.         Connection $connection,
  54.         SalesChannelRepositoryInterface $salesChannelProductRepository,
  55.         EntityRepositoryInterface $propertyGroupOptionRepository,
  56.         SystemConfigService $systemConfigService,
  57.         Translator $translator
  58.     )
  59.     {
  60.         $this->connection $connection;
  61.         $this->salesChannelProductRepository $salesChannelProductRepository;
  62.         $this->propertyGroupOptionRepository $propertyGroupOptionRepository;
  63.         $this->systemConfigService $systemConfigService;
  64.         $this->translator $translator;
  65.     }
  66.     public static function getSubscribedEvents(): array
  67.     {
  68.         return [
  69.             CheckoutFinishPageLoadedEvent::class => 'onPageLoaded'
  70.         ];
  71.     }
  72.     public function onPageLoaded(CheckoutFinishPageLoadedEvent $event): void
  73.     {
  74.         if (!$this->systemConfigService->get('CogiCmsFinishPage.config.active')) {
  75.             return;
  76.         }
  77.         $page $event->getPage();
  78.         $order $page->getOrder();
  79.         $lineItems $order->getLineItems();
  80.         $salesChannelId $event->getSalesChannelContext()->getSalesChannel()->getId();
  81.         // Use property cross selling as default if no other cross selling defined
  82.         $crossSellings = new CrossSellingElementCollection();
  83.         $crossSellingElement = new CrossSellingElement();
  84.         $crossSelling = new ProductCrossSellingEntity();
  85.         $defaultName $this->translator->trans('cogiPropertyCrossSelling.defaultCrossSellingName');
  86.         $crossSelling->setName($defaultName);
  87.         $crossSelling->setTranslated(['name' => $defaultName]);
  88.         $crossSelling->setType('cogiPropertyCrossSelling');
  89.         $crossSelling->setId(md5('crossSelling' $order->getId()));
  90.         $crossSelling->setActive(true);
  91.         $useManufacturer $this->systemConfigService->get('CogiPropertyCrossSelling.config.useManufacturer'$salesChannelId);
  92.         $manufacturerMustMatch $this->systemConfigService->get('CogiPropertyCrossSelling.config.manufacturerMustMatch'$salesChannelId);
  93.         // Get the property filter
  94.         $propertyFilter $this->getPropertyFilter($lineItems$salesChannelId$event->getSalesChannelContext());
  95.         $filter $propertyFilter->getFilter();
  96.         // If manufacturer should be used and is set for product, get filter and set connection accordingly
  97.         if ($useManufacturer && (array_filter(array_map(function($lineItem) { return (($payload $lineItem->getPayload()) && !empty($payload['manufacturerId'])) ? $payload['manufacturerId'] : null; }, $lineItems->getElements())) || $manufacturerMustMatch)) {
  98.             $manufacturerFilter $this->getManufacturerFilter($lineItems);
  99.             $filterConnection $manufacturerMustMatch MultiFilter::CONNECTION_AND MultiFilter::CONNECTION_OR;
  100.             $filter = new MultiFilter($filterConnection, [
  101.                 $manufacturerFilter->getFilter(),
  102.                 $propertyFilter->getFilter()
  103.             ]);
  104.         }
  105.         // Configure Criteria
  106.         $criteria = new Criteria();
  107.         $criteria->setLimit($this->systemConfigService->get('CogiPropertyCrossSelling.config.limit'$salesChannelId) ?: 15);
  108.         $criteria->addFilter($filter);
  109.         // Make sure current product is not shown in cross selling
  110.         $criteria->addFilter(new NotFilter(
  111.             NotFilter::CONNECTION_AND,
  112.             [new EqualsAnyFilter('id'array_values(array_map(function($lineItem) { return $lineItem->getProductId(); },$lineItems->getElements())))]
  113.         ));
  114.         // Exclude variants of the same product
  115.         $parentIds array_values(array_filter(array_map(function($lineItem) { return (($payload $lineItem->getPayload()) && !empty($payload['parentId'])) ? $payload['parentId'] : null; },$lineItems->getElements())));
  116.         if ($parentIds) {
  117.             $criteria->addFilter(new MultiFilter(
  118.                 MultiFilter::CONNECTION_OR,
  119.                 [
  120.                     new NotFilter(
  121.                         NotFilter::CONNECTION_OR,
  122.                         [new EqualsAnyFilter('parentId'$parentIds)]
  123.                     ),
  124.                     new EqualsFilter(
  125.                         'parentId',
  126.                         null
  127.                     )
  128.                 ]
  129.             ));
  130.             $criteria->addFilter(new NotFilter(
  131.                 NotFilter::CONNECTION_AND,
  132.                 [new EqualsAnyFilter('id'$parentIds)]
  133.             ));
  134.         }
  135.         // Exclude unavailable products if set in config
  136.         if ($this->systemConfigService->get('CogiPropertyCrossSelling.config.excludeUnavailable'$salesChannelId)) {
  137.             $criteria->addFilter(new ProductCloseoutFilter());
  138.         }
  139.         // Get products and set for cross selling
  140.         $result $this->salesChannelProductRepository->search($criteria$event->getSalesChannelContext());
  141.         $products $result->getEntities();
  142.         $crossSellingElement->setProducts($products);
  143.         $crossSellingElement->setCrossSelling($crossSelling);
  144.         $crossSellingElement->setTotal(1);
  145.         $crossSellings->add($crossSellingElement);
  146.         $page->cogiCrossSellings $crossSellings;
  147.     }
  148.     protected function getManufacturerFilter(OrderLineItemCollection $lineItems)
  149.     {
  150.         $manufacturerIds array_values(array_filter(array_map(function($lineItem) { return (($payload $lineItem->getPayload()) && !empty($payload['manufacturerId'])) ? $payload['manufacturerId'] : null; }, $lineItems->getElements())));
  151.         // If products have no manufacturer, use NULL for filter
  152.         $filter = new EqualsFilter('product.manufacturerId'null);
  153.         if (!empty($manufacturerIds)) {
  154.             $filter = new EqualsAnyFilter('product.manufacturerId'$manufacturerIds);
  155.         }
  156.         return new Filter(
  157.             'manufacturer',
  158.             !empty($manufacturerIds),
  159.             [new EntityAggregation('manufacturer''product.manufacturerId''product_manufacturer')],
  160.             $filter,
  161.             $manufacturerIds
  162.         );
  163.     }
  164.     protected function getPropertyFilter(OrderLineItemCollection $lineItems$salesChannelId$salesChannelContext)
  165.     {
  166.         $ids = [];
  167.         foreach ($lineItems as $lineItem) {
  168.             $payload $lineItem->getPayload();
  169.             if (!empty($payload['optionIds'])) {
  170.                 $ids array_merge($ids$payload['optionIds']);
  171.             }
  172.             if (!empty($payload['propertyIds'])) {
  173.                 $ids array_merge($ids$payload['propertyIds']);
  174.             }
  175.         }
  176.         // Filter by property groups defined in config
  177.         $criteria = new Criteria($ids);
  178.         $result $this->propertyGroupOptionRepository->search($criteria$salesChannelContext->getContext());
  179.         $configProperties $this->systemConfigService->get('CogiPropertyCrossSelling.config.properties'$salesChannelId);
  180.         $ids array_filter($ids, function($propertyId) use ($result$configProperties) {
  181.             $property $result->get($propertyId);
  182.             return $property && in_array($property->getGroupId(), $configProperties ?: []);
  183.         });
  184.         // Set the filter
  185.         $propertyAggregation = new TermsAggregation('properties''product.properties.id');
  186.         $optionAggregation = new TermsAggregation('options''product.options.id');
  187.         $grouped $this->connection->fetchAllAssociative(
  188.             'SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id FROM property_group_option WHERE id IN (:ids)',
  189.             ['ids' => Uuid::fromHexToBytesList($ids)],
  190.             ['ids' => Connection::PARAM_STR_ARRAY]
  191.         );
  192.         $grouped FetchModeHelper::group($grouped);
  193.         $filters = [];
  194.         foreach ($grouped as $options) {
  195.             $options array_column($options'id');
  196.             $filters[] = new MultiFilter(
  197.                 MultiFilter::CONNECTION_OR,
  198.                 [
  199.                     new EqualsAnyFilter('product.optionIds'$options),
  200.                     new EqualsAnyFilter('product.propertyIds'$options),
  201.                 ]
  202.             );
  203.         }
  204.         $op $this->systemConfigService->get('CogiPropertyCrossSelling.config.propertyLogic'$salesChannelId) ?: 'or';
  205.         $operator MultiFilter::CONNECTION_OR;
  206.         if (strtolower($op) === 'and') {
  207.             $operator MultiFilter::CONNECTION_AND;
  208.         }
  209.         return new Filter(
  210.             'properties',
  211.             true,
  212.             [$propertyAggregation$optionAggregation],
  213.             new MultiFilter($operator$filters),
  214.             $ids,
  215.             false
  216.         );
  217.     }
  218. }