custom/plugins/IntediaDoofinderSW6/src/Storefront/Subscriber/SearchSubscriber.php line 247

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Intedia\Doofinder\Storefront\Subscriber;
  3. use Intedia\Doofinder\Core\Content\Settings\Service\BotDetectionHandler;
  4. use Intedia\Doofinder\Core\Content\Settings\Service\SettingsHandler;
  5. use Intedia\Doofinder\Doofinder\Api\Search;
  6. use Psr\Log\LoggerInterface;
  7. use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
  8. use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
  9. use Shopware\Core\Content\Product\ProductEntity;
  10. use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult;
  11. use Shopware\Core\Framework\Context;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  19. use Shopware\Core\Framework\Struct\ArrayStruct;
  20. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  21. use Shopware\Core\System\SystemConfig\SystemConfigService;
  22. use Shopware\Storefront\Page\Search\SearchPageLoadedEvent;
  23. use Shopware\Storefront\Page\Suggest\SuggestPageLoadedEvent;
  24. use Shopware\Storefront\Pagelet\Footer\FooterPageletLoadedEvent;
  25. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  26. use Symfony\Component\HttpFoundation\Request;
  27. class SearchSubscriber implements EventSubscriberInterface
  28. {
  29.     const IS_DOOFINDER_TERM 'doofinder-search';
  30.     /** @var SystemConfigService */
  31.     protected $systemConfigService;
  32.     /** @var LoggerInterface */
  33.     protected $logger;
  34.     /** @var Search */
  35.     protected $searchApi;
  36.     /** @var array */
  37.     protected $doofinderIds;
  38.     /** @var integer */
  39.     protected $shopwareLimit;
  40.     /** @var integer */
  41.     protected $shopwareOffset;
  42.     /** @var bool */
  43.     protected $isScoreSorting;
  44.     /** @var bool */
  45.     protected $isSuggestCall false;
  46.     private EntityRepository $salesChannelDomainRepository;
  47.     private EntityRepository $productRepository;
  48.     private SettingsHandler $settingsHandler;
  49.     /**
  50.      * SearchSubscriber constructor.
  51.      * @param SystemConfigService $systemConfigService
  52.      * @param LoggerInterface $logger
  53.      * @param Search $searchApi
  54.      * @param EntityRepository $salesChannelDomainRepository
  55.      * @param EntityRepository $productRepository
  56.      * @param SettingsHandler $settingsHandler
  57.      */
  58.     public function __construct(
  59.         SystemConfigService $systemConfigService,
  60.         LoggerInterface $logger,
  61.         Search   $searchApi,
  62.         EntityRepository $salesChannelDomainRepository,
  63.         EntityRepository $productRepository,
  64.         SettingsHandler $settingsHandler
  65.     ) {
  66.         $this->systemConfigService          $systemConfigService;
  67.         $this->logger                       $logger;
  68.         $this->searchApi                    $searchApi;
  69.         $this->salesChannelDomainRepository $salesChannelDomainRepository;
  70.         $this->productRepository            $productRepository;
  71.         $this->settingsHandler              $settingsHandler;
  72.     }
  73.     /**
  74.      * {@inheritdoc}
  75.      */
  76.     public static function getSubscribedEvents(): array
  77.     {
  78.         return [
  79.             ProductSearchCriteriaEvent::class  => 'onSearchCriteriaEvent',
  80.             SearchPageLoadedEvent::class       => 'onSearchPageLoadedEvent',
  81.             ProductSuggestCriteriaEvent::class => 'onSuggestCriteriaEvent',
  82.             SuggestPageLoadedEvent::class      => 'onSuggestPageLoadedEvent',
  83.             FooterPageletLoadedEvent::class    => 'generateCorrectDooFinderData'
  84.         ];
  85.     }
  86.     public function generateCorrectDooFinderData(FooterPageletLoadedEvent $event)
  87.     {
  88.         $criteria = new Criteria([$event->getSalesChannelContext()->getDomainId()]);
  89.         $criteria->addAssociation('language')
  90.             ->addAssociation('currency')
  91.             ->addAssociation('language.locale')
  92.             ->addAssociation('domains.language.locale');
  93.         $domain $this->salesChannelDomainRepository->search($criteriaContext::createDefaultContext())->first();
  94.         $doofinderLayer $this->settingsHandler->getDooFinderLayer($domain);
  95.         $hashId '';
  96.         $storeId '';
  97.         if ($doofinderLayer) {
  98.             $hashId $doofinderLayer->getDooFinderHashId();
  99.             $storeId $doofinderLayer->getDoofinderStoreId();
  100.         }
  101.         $event->getPagelet()->addExtension('doofinder', new ArrayStruct(['hashId' => $hashId'storeId' => $storeId]));
  102.     }
  103.     /**
  104.      * @param ProductSearchCriteriaEvent $event
  105.      */
  106.     public function onSearchCriteriaEvent(ProductSearchCriteriaEvent $event): void
  107.     {
  108.         $criteria $event->getCriteria();
  109.         $request  $event->getRequest();
  110.         $context  $event->getSalesChannelContext();
  111.         $this->handleWithDoofinder($context$request$criteria);
  112.     }
  113.     /**
  114.      * @param ProductSuggestCriteriaEvent $event
  115.      */
  116.     public function onSuggestCriteriaEvent(ProductSuggestCriteriaEvent $event): void
  117.     {
  118.         $criteria $event->getCriteria();
  119.         $request  $event->getRequest();
  120.         $context  $event->getSalesChannelContext();
  121.         $this->isSuggestCall true;
  122.         $this->handleWithDoofinder($context$request$criteria);
  123.     }
  124.     /**
  125.      * @param SalesChannelContext $context
  126.      * @param Request $request
  127.      * @param Criteria $criteria
  128.      */
  129.     protected function handleWithDoofinder(SalesChannelContext $contextRequest $requestCriteria $criteria): void
  130.     {
  131.         $searchSubscriberActivationMode $this->getDoofinderSearchSubscriberActivationMode($context);
  132.         // inactive for bots
  133.         if ($searchSubscriberActivationMode == && BotDetectionHandler::checkIfItsBot($request->headers->get('User-Agent'))) {
  134.             return;
  135.         } elseif ($searchSubscriberActivationMode == 3) { // inactive for all
  136.             return;
  137.         }
  138.         if ($this->systemConfigService->get('IntediaDoofinderSW6.config.doofinderEnabled'$context $context->getSalesChannel()->getId() : null)) {
  139.             $term $request->query->get('search');
  140.             if ($term) {
  141.                 $this->doofinderIds $this->searchApi->queryIds($term$context);
  142.                 $this->storeShopwareLimitAndOffset($criteria);
  143.                 if (!empty($this->doofinderIds)) {
  144.                     $this->manipulateCriteriaLimitAndOffset($criteria);
  145.                     $this->resetCriteriaFiltersQueriesAndSorting($criteria);
  146.                     $this->addProductNumbersToCriteria($criteria);
  147.                     $criteria->setTerm(self::IS_DOOFINDER_TERM);
  148.                 }
  149.             }
  150.         }
  151.     }
  152.     /**
  153.      * @param Criteria $criteria
  154.      */
  155.     protected function resetCriteriaFiltersQueriesAndSorting(Criteria $criteria): void
  156.     {
  157.         $criteria->resetFilters();
  158.         $criteria->resetQueries();
  159.         if ($this->isSuggestCall || $this->checkIfScoreSorting($criteria)) {
  160.             $criteria->resetSorting();
  161.         }
  162.     }
  163.     /**
  164.      * @param Criteria $criteria
  165.      * @return bool
  166.      */
  167.     protected function checkIfScoreSorting(Criteria $criteria)
  168.     {
  169.         /** @var FieldSorting */
  170.         $sorting = !empty($criteria->getSorting()) ? $criteria->getSorting()[0] : null;
  171.         if ($sorting) {
  172.             $this->isScoreSorting $sorting->getField() === '_score';
  173.         }
  174.         return $this->isScoreSorting;
  175.     }
  176.     /**
  177.      * @param Criteria $criteria
  178.      */
  179.     protected function addProductNumbersToCriteria(Criteria $criteria): void
  180.     {
  181.         if ($this->isAssocArray($this->doofinderIds)) {
  182.             $criteria->addFilter(
  183.                 new OrFilter([
  184.                     new EqualsAnyFilter('productNumber'array_values($this->doofinderIds)),
  185.                     new EqualsAnyFilter('parent.productNumber'array_keys($this->doofinderIds)),
  186.                     new EqualsAnyFilter('productNumber'array_keys($this->doofinderIds))
  187.                 ])
  188.             );
  189.         }
  190.         else {
  191.             $criteria->addFilter(new EqualsAnyFilter('productNumber'array_values($this->doofinderIds)));
  192.         }
  193.     }
  194.     /**
  195.      * @param array $arr
  196.      * @return bool
  197.      */
  198.     protected function isAssocArray(array $arr)
  199.     {
  200.         if (array() === $arr)
  201.             return false;
  202.         return array_keys($arr) !== range(0count($arr) - 1);
  203.     }
  204.     /**
  205.      * @param SearchPageLoadedEvent $event
  206.      */
  207.     public function onSearchPageLoadedEvent(SearchPageLoadedEvent $event): void
  208.     {
  209.         $event->getPage()->setListing($this->modifyListing($event->getPage()->getListing()));
  210.     }
  211.     /**
  212.      * @param SuggestPageLoadedEvent $event
  213.      */
  214.     public function onSuggestPageLoadedEvent(SuggestPageLoadedEvent $event): void
  215.     {
  216.         $event->getPage()->setSearchResult($this->modifyListing($event->getPage()->getSearchResult()));
  217.     }
  218.     /**
  219.      * @param EntitySearchResult $listing
  220.      * @return object|ProductListingResult
  221.      */
  222.     protected function modifyListing(EntitySearchResult $listing)
  223.     {
  224.         if ($listing && !empty($this->doofinderIds)) {
  225.             // reorder entities if doofinder score sorting
  226.             if ($this->isSuggestCall || $this->isScoreSorting) {
  227.                 $this->orderByProductNumberArray($listing->getEntities(), $listing->getContext());
  228.             }
  229.             $newListing ProductListingResult::createFrom(new EntitySearchResult(
  230.                 $listing->getEntity(),
  231.                 $listing->getTotal(),
  232.                 $this->sliceEntityCollection($listing->getEntities(), $this->shopwareOffset$this->shopwareLimit),
  233.                 $listing->getAggregations(),
  234.                 $listing->getCriteria(),
  235.                 $listing->getContext()
  236.             ));
  237.             $newListing->setExtensions($listing->getExtensions());
  238.             $this->reintroduceShopwareLimitAndOffset($newListing);
  239.             if ($this->isSuggestCall == false && $listing instanceof ProductListingResult) {
  240.                 $newListing->setSorting($listing->getSorting());
  241.                 if (method_exists($listing"getAvailableSortings") && method_exists($newListing"setAvailableSortings")) {
  242.                     $newListing->setAvailableSortings($listing->getAvailableSortings());
  243.                 }
  244.                 else if (method_exists($listing"getSortings") && method_exists($newListing"setSortings")) {
  245.                     $newListing->setSortings($listing->getSortings());
  246.                 }
  247.             }
  248.             return $newListing;
  249.         }
  250.         return $listing;
  251.     }
  252.     /**
  253.      * @param EntityCollection $collection
  254.      * @param Context $context
  255.      * @return EntityCollection
  256.      */
  257.     protected function orderByProductNumberArray(EntityCollection $collectionContext $context): EntityCollection
  258.     {
  259.         if ($collection) {
  260.             $sortingNumbers  array_keys($this->doofinderIds);
  261.             $fallbackNumbers array_values($this->doofinderIds);
  262.             $parentIds       $collection->filter(function(ProductEntity $product) { return !!$product->getParentId(); })->map(function(ProductEntity $product) { return $product->getParentId(); });
  263.             $parentNumbers   $this->getParentNumbers($parentIds$context);
  264.             $collection->sort(
  265.                 function (ProductEntity $aProductEntity $b) use ($sortingNumbers$fallbackNumbers$parentNumbers) {
  266.                     $aIndex false;
  267.                     $bIndex false;
  268.                     if ($parentNumbers[$a->getParentId()] || $parentNumbers[$b->getParentId()]) {
  269.                         $aIndex array_search($parentNumbers[$a->getParentId()], $sortingNumbers);
  270.                         $bIndex array_search($parentNumbers[$b->getParentId()], $sortingNumbers);
  271.                     }
  272.                     if ($aIndex === false || $bIndex === false) {
  273.                         $aIndex $aIndex !== false $aIndex array_search($a->getId(), $sortingNumbers);
  274.                         $bIndex $bIndex !== false $bIndex array_search($b->getId(), $sortingNumbers);
  275.                     }
  276.                     if ($aIndex === false || $bIndex === false) {
  277.                         $aIndex $aIndex !== false $aIndex array_search($a->getProductNumber(), $fallbackNumbers);
  278.                         $bIndex $bIndex !== false $bIndex array_search($b->getProductNumber(), $fallbackNumbers);
  279.                     }
  280.                     return ($aIndex !== false $aIndex PHP_INT_MAX) - ($bIndex !== false $bIndex PHP_INT_MAX); }
  281.             );
  282.         }
  283.         return $collection;
  284.     }
  285.     /**
  286.      * @param array $parentIds
  287.      * @param Context $context
  288.      * @return array
  289.      */
  290.     protected function getParentNumbers(array $parentIdsContext $context): array
  291.     {
  292.         if (empty($parentIds)) {
  293.             return [];
  294.         }
  295.         $parentNumbers = [];
  296.         /** @var ProductEntity $parent */
  297.         foreach ($this->productRepository->search(new Criteria($parentIds), $context) as $parent) {
  298.             $parentNumbers[$parent->getId()] = $parent->getProductNumber();
  299.         }
  300.         return $parentNumbers;
  301.     }
  302.     /**
  303.      * @param Criteria $criteria
  304.      */
  305.     protected function storeShopwareLimitAndOffset(Criteria $criteria): void
  306.     {
  307.         $this->shopwareLimit  $criteria->getLimit();
  308.         $this->shopwareOffset $criteria->getOffset();
  309.     }
  310.     /**
  311.      * @param Criteria $criteria
  312.      */
  313.     protected function manipulateCriteriaLimitAndOffset(Criteria $criteria): void
  314.     {
  315.         $criteria->setLimit(count($this->doofinderIds));
  316.         $criteria->setOffset(0);
  317.     }
  318.     /**
  319.      * @param ProductListingResult $newListing
  320.      */
  321.     protected function reintroduceShopwareLimitAndOffset(ProductListingResult $newListing): void
  322.     {
  323.         $newListing->setLimit($this->shopwareLimit);
  324.         $newListing->getCriteria()->setLimit($this->shopwareLimit);
  325.         $newListing->getCriteria()->setOffset($this->shopwareOffset);
  326.     }
  327.     /**
  328.      * @param EntityCollection $collection
  329.      * @param $offset
  330.      * @param $limit
  331.      * @return EntityCollection
  332.      */
  333.     protected function sliceEntityCollection(EntityCollection $collection$offset$limit): EntityCollection
  334.     {
  335.         $iterator    $collection->getIterator();
  336.         $newEntities = [];
  337.         $i 0;
  338.         for ($iterator->rewind(); $iterator->valid(); $iterator->next()) {
  339.             if ($i >= $offset && $i $offset $limit) {
  340.                 $newEntities[] = $iterator->current();
  341.             }
  342.             $i++;
  343.         }
  344.         return new EntityCollection($newEntities);
  345.     }
  346.     /**
  347.      * @param SalesChannelContext $context
  348.      * @return array|bool|float|int|string|null
  349.      */
  350.     protected function getDoofinderSearchSubscriberActivationMode(SalesChannelContext $context)
  351.     {
  352.         $doofinderSearchSubscriberActivate $this->systemConfigService->get(
  353.             'IntediaDoofinderSW6.config.doofinderSearchSubscriberActivate',
  354.             $context $context->getSalesChannel()->getId() : null
  355.         );
  356.         return $doofinderSearchSubscriberActivate;
  357.     }
  358. }