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

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