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

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