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

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->getSalesChannel()->getId())) {
  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.                     if ($this->isSuggestCall) {
  148.                         $criteria->setTerm(null);
  149.                     }
  150.                     else {
  151.                         $criteria->setTerm(self::IS_DOOFINDER_TERM);
  152.                     }
  153.                 }
  154.             }
  155.         }
  156.     }
  157.     /**
  158.      * @param Criteria $criteria
  159.      */
  160.     protected function resetCriteriaFiltersQueriesAndSorting(Criteria $criteria): void
  161.     {
  162.         $criteria->resetFilters();
  163.         $criteria->resetQueries();
  164.         if ($this->isSuggestCall || $this->checkIfScoreSorting($criteria)) {
  165.             $criteria->resetSorting();
  166.         }
  167.     }
  168.     /**
  169.      * @param Criteria $criteria
  170.      * @return bool
  171.      */
  172.     protected function checkIfScoreSorting(Criteria $criteria)
  173.     {
  174.         /** @var FieldSorting */
  175.         $sorting = !empty($criteria->getSorting()) ? $criteria->getSorting()[0] : null;
  176.         if ($sorting) {
  177.             $this->isScoreSorting $sorting->getField() === '_score';
  178.         }
  179.         return $this->isScoreSorting;
  180.     }
  181.     /**
  182.      * @param Criteria $criteria
  183.      */
  184.     protected function addProductNumbersToCriteria(Criteria $criteria): void
  185.     {
  186.         if ($this->isAssocArray($this->doofinderIds)) {
  187.             $criteria->addFilter(
  188.                 new OrFilter([
  189.                     new EqualsAnyFilter('productNumber'array_keys($this->doofinderIds)),
  190.                     new EqualsAnyFilter('productNumber'array_values($this->doofinderIds))
  191.                 ])
  192.             );
  193.         }
  194.         else {
  195.             $criteria->addFilter(new EqualsAnyFilter('productNumber'array_values($this->doofinderIds)));
  196.         }
  197.     }
  198.     /**
  199.      * @param array $arr
  200.      * @return bool
  201.      */
  202.     protected function isAssocArray(array $arr)
  203.     {
  204.         if (array() === $arr)
  205.             return false;
  206.         return array_keys($arr) !== range(0count($arr) - 1);
  207.     }
  208.     /**
  209.      * @param SearchPageLoadedEvent $event
  210.      */
  211.     public function onSearchPageLoadedEvent(SearchPageLoadedEvent $event): void
  212.     {
  213.         $event->getPage()->setListing($this->modifyListing($event->getPage()->getListing()));
  214.     }
  215.     /**
  216.      * @param SuggestPageLoadedEvent $event
  217.      */
  218.     public function onSuggestPageLoadedEvent(SuggestPageLoadedEvent $event): void
  219.     {
  220.         $event->getPage()->setSearchResult($this->modifyListing($event->getPage()->getSearchResult()));
  221.     }
  222.     /**
  223.      * @param EntitySearchResult $listing
  224.      * @return object|ProductListingResult
  225.      */
  226.     protected function modifyListing(EntitySearchResult $listing)
  227.     {
  228.         if ($listing && !empty($this->doofinderIds)) {
  229.             // reorder entities if doofinder score sorting
  230.             if ($this->isSuggestCall || $this->isScoreSorting) {
  231.                 $this->orderByProductNumberArray($listing->getEntities(), $listing->getContext());
  232.             }
  233.             $newListing ProductListingResult::createFrom(new EntitySearchResult(
  234.                 $listing->getEntity(),
  235.                 $listing->getTotal(),
  236.                 $this->sliceEntityCollection($listing->getEntities(), $this->shopwareOffset$this->shopwareLimit),
  237.                 $listing->getAggregations(),
  238.                 $listing->getCriteria(),
  239.                 $listing->getContext()
  240.             ));
  241.             $newListing->setExtensions($listing->getExtensions());
  242.             $this->reintroduceShopwareLimitAndOffset($newListing);
  243.             if ($this->isSuggestCall == false && $listing instanceof ProductListingResult) {
  244.                 $newListing->setSorting($listing->getSorting());
  245.                 if (method_exists($listing"getAvailableSortings") && method_exists($newListing"setAvailableSortings")) {
  246.                     $newListing->setAvailableSortings($listing->getAvailableSortings());
  247.                 }
  248.                 else if (method_exists($listing"getSortings") && method_exists($newListing"setSortings")) {
  249.                     $newListing->setSortings($listing->getSortings());
  250.                 }
  251.             }
  252.             return $newListing;
  253.         }
  254.         return $listing;
  255.     }
  256.     /**
  257.      * @param EntityCollection $collection
  258.      * @param Context $context
  259.      * @return EntityCollection
  260.      */
  261.     protected function orderByProductNumberArray(EntityCollection $collectionContext $context): EntityCollection
  262.     {
  263.         if ($collection) {
  264.             $productNumbers array_keys($this->doofinderIds);
  265.             $groupNumbers   array_values($this->doofinderIds);
  266.             $parentIds      $collection->filter(function(ProductEntity $product) { return !!$product->getParentId(); })->map(function(ProductEntity $product) { return $product->getParentId(); });
  267.             $parentNumbers  $this->getParentNumbers($parentIds$context);
  268.             $collection->sort(
  269.                 function (ProductEntity $aProductEntity $b) use ($productNumbers$groupNumbers$parentNumbers) {
  270.                     $aIndex array_search($a->getProductNumber(), $productNumbers);
  271.                     $bIndex array_search($b->getProductNumber(), $productNumbers);
  272.                     // order by product number and search parents
  273.                     if (($aIndex === false || $bIndex === false) && ($parentNumbers[$a->getParentId()] || $parentNumbers[$b->getParentId()])) {
  274.                         $aIndex array_search($parentNumbers[$a->getParentId()], $productNumbers);
  275.                         $bIndex array_search($parentNumbers[$b->getParentId()], $productNumbers);
  276.                     }
  277.                     // order by group number and search parents
  278.                     if (($aIndex === false || $bIndex === false) && ($parentNumbers[$a->getParentId()] || $parentNumbers[$b->getParentId()])) {
  279.                         $aIndex array_search($parentNumbers[$a->getParentId()], $groupNumbers);
  280.                         $bIndex array_search($parentNumbers[$b->getParentId()], $groupNumbers);
  281.                     }
  282.                     return ($aIndex !== false $aIndex PHP_INT_MAX) - ($bIndex !== false $bIndex PHP_INT_MAX); }
  283.             );
  284.         }
  285.         return $collection;
  286.     }
  287.     /**
  288.      * @param array $parentIds
  289.      * @param Context $context
  290.      * @return array
  291.      */
  292.     protected function getParentNumbers(array $parentIdsContext $context): array
  293.     {
  294.         if (empty($parentIds)) {
  295.             return [];
  296.         }
  297.         $parentNumbers = [];
  298.         /** @var ProductEntity $parent */
  299.         foreach ($this->productRepository->search(new Criteria($parentIds), $context) as $parent) {
  300.             $parentNumbers[$parent->getId()] = $parent->getProductNumber();
  301.         }
  302.         return $parentNumbers;
  303.     }
  304.     /**
  305.      * @param Criteria $criteria
  306.      */
  307.     protected function storeShopwareLimitAndOffset(Criteria $criteria): void
  308.     {
  309.         $this->shopwareLimit  $criteria->getLimit();
  310.         $this->shopwareOffset $criteria->getOffset();
  311.     }
  312.     /**
  313.      * @param Criteria $criteria
  314.      */
  315.     protected function manipulateCriteriaLimitAndOffset(Criteria $criteria): void
  316.     {
  317.         $criteria->setLimit(count($this->doofinderIds));
  318.         $criteria->setOffset(0);
  319.     }
  320.     /**
  321.      * @param ProductListingResult $newListing
  322.      */
  323.     protected function reintroduceShopwareLimitAndOffset(ProductListingResult $newListing): void
  324.     {
  325.         $newListing->setLimit($this->shopwareLimit);
  326.         $newListing->getCriteria()->setLimit($this->shopwareLimit);
  327.         $newListing->getCriteria()->setOffset($this->shopwareOffset);
  328.     }
  329.     /**
  330.      * @param EntityCollection $collection
  331.      * @param $offset
  332.      * @param $limit
  333.      * @return EntityCollection
  334.      */
  335.     protected function sliceEntityCollection(EntityCollection $collection$offset$limit): EntityCollection
  336.     {
  337.         $iterator    $collection->getIterator();
  338.         $newEntities = [];
  339.         $i 0;
  340.         for ($iterator->rewind(); $iterator->valid(); $iterator->next()) {
  341.             if ($i >= $offset && $i $offset $limit) {
  342.                 $newEntities[] = $iterator->current();
  343.             }
  344.             $i++;
  345.         }
  346.         return new EntityCollection($newEntities);
  347.     }
  348.     /**
  349.      * @param SalesChannelContext $context
  350.      * @return array|bool|float|int|string|null
  351.      */
  352.     protected function getDoofinderSearchSubscriberActivationMode(SalesChannelContext $context)
  353.     {
  354.         $doofinderSearchSubscriberActivate $this->systemConfigService->get(
  355.             'IntediaDoofinderSW6.config.doofinderSearchSubscriberActivate',
  356.             $context $context->getSalesChannel()->getId() : null
  357.         );
  358.         return $doofinderSearchSubscriberActivate;
  359.     }
  360. }