<?php declare(strict_types=1);namespace Intedia\Doofinder\Storefront\Subscriber;use Intedia\Doofinder\Core\Content\Settings\Service\BotDetectionHandler;use Intedia\Doofinder\Core\Content\Settings\Service\SettingsHandler;use Intedia\Doofinder\Doofinder\Api\Search;use Psr\Log\LoggerInterface;use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;use Shopware\Core\Content\Product\ProductEntity;use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult;use Shopware\Core\Framework\Context;use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;use Shopware\Core\Framework\Struct\ArrayStruct;use Shopware\Core\System\SalesChannel\SalesChannelContext;use Shopware\Core\System\SystemConfig\SystemConfigService;use Shopware\Storefront\Page\Search\SearchPageLoadedEvent;use Shopware\Storefront\Page\Suggest\SuggestPageLoadedEvent;use Shopware\Storefront\Pagelet\Footer\FooterPageletLoadedEvent;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\HttpFoundation\Request;class SearchSubscriber implements EventSubscriberInterface{ const IS_DOOFINDER_TERM = 'doofinder-search'; /** @var SystemConfigService */ protected $systemConfigService; /** @var LoggerInterface */ protected $logger; /** @var Search */ protected $searchApi; /** @var array */ protected $doofinderIds; /** @var integer */ protected $shopwareLimit; /** @var integer */ protected $shopwareOffset; /** @var bool */ protected $isScoreSorting; /** @var bool */ protected $isSuggestCall = false; private EntityRepository $salesChannelDomainRepository; private EntityRepository $productRepository; private SettingsHandler $settingsHandler; /** * SearchSubscriber constructor. * @param SystemConfigService $systemConfigService * @param LoggerInterface $logger * @param Search $searchApi * @param EntityRepository $salesChannelDomainRepository * @param EntityRepository $productRepository * @param SettingsHandler $settingsHandler */ public function __construct( SystemConfigService $systemConfigService, LoggerInterface $logger, Search $searchApi, EntityRepository $salesChannelDomainRepository, EntityRepository $productRepository, SettingsHandler $settingsHandler ) { $this->systemConfigService = $systemConfigService; $this->logger = $logger; $this->searchApi = $searchApi; $this->salesChannelDomainRepository = $salesChannelDomainRepository; $this->productRepository = $productRepository; $this->settingsHandler = $settingsHandler; } /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { return [ ProductSearchCriteriaEvent::class => 'onSearchCriteriaEvent', SearchPageLoadedEvent::class => 'onSearchPageLoadedEvent', ProductSuggestCriteriaEvent::class => 'onSuggestCriteriaEvent', SuggestPageLoadedEvent::class => 'onSuggestPageLoadedEvent', FooterPageletLoadedEvent::class => 'generateCorrectDooFinderData' ]; } public function generateCorrectDooFinderData(FooterPageletLoadedEvent $event) { $criteria = new Criteria([$event->getSalesChannelContext()->getDomainId()]); $criteria->addAssociation('language') ->addAssociation('currency') ->addAssociation('language.locale') ->addAssociation('domains.language.locale'); $domain = $this->salesChannelDomainRepository->search($criteria, Context::createDefaultContext())->first(); $doofinderLayer = $this->settingsHandler->getDooFinderLayer($domain); $hashId = ''; $storeId = ''; if ($doofinderLayer) { $hashId = $doofinderLayer->getDooFinderHashId(); $storeId = $doofinderLayer->getDoofinderStoreId(); } $event->getPagelet()->addExtension('doofinder', new ArrayStruct(['hashId' => $hashId, 'storeId' => $storeId])); } /** * @param ProductSearchCriteriaEvent $event */ public function onSearchCriteriaEvent(ProductSearchCriteriaEvent $event): void { $criteria = $event->getCriteria(); $request = $event->getRequest(); $context = $event->getSalesChannelContext(); $this->handleWithDoofinder($context, $request, $criteria); } /** * @param ProductSuggestCriteriaEvent $event */ public function onSuggestCriteriaEvent(ProductSuggestCriteriaEvent $event): void { $criteria = $event->getCriteria(); $request = $event->getRequest(); $context = $event->getSalesChannelContext(); $this->isSuggestCall = true; $this->handleWithDoofinder($context, $request, $criteria); } /** * @param SalesChannelContext $context * @param Request $request * @param Criteria $criteria */ protected function handleWithDoofinder(SalesChannelContext $context, Request $request, Criteria $criteria): void { $searchSubscriberActivationMode = $this->getDoofinderSearchSubscriberActivationMode($context); // inactive for bots if ($searchSubscriberActivationMode == 2 && BotDetectionHandler::checkIfItsBot($request->headers->get('User-Agent'))) { return; } elseif ($searchSubscriberActivationMode == 3) { // inactive for all return; } if ($this->systemConfigService->get('IntediaDoofinderSW6.config.doofinderEnabled', $context->getSalesChannel()->getId())) { $term = $request->query->get('search'); if ($term) { $this->doofinderIds = $this->searchApi->queryIds($term, $context); $this->storeShopwareLimitAndOffset($criteria); if (!empty($this->doofinderIds)) { $this->manipulateCriteriaLimitAndOffset($criteria); $this->resetCriteriaFiltersQueriesAndSorting($criteria); $this->addProductNumbersToCriteria($criteria); if ($this->isSuggestCall) { $criteria->setTerm(null); } else { $criteria->setTerm(self::IS_DOOFINDER_TERM); } } } } } /** * @param Criteria $criteria */ protected function resetCriteriaFiltersQueriesAndSorting(Criteria $criteria): void { $criteria->resetFilters(); $criteria->resetQueries(); if ($this->isSuggestCall || $this->checkIfScoreSorting($criteria)) { $criteria->resetSorting(); } } /** * @param Criteria $criteria * @return bool */ protected function checkIfScoreSorting(Criteria $criteria) { /** @var FieldSorting */ $sorting = !empty($criteria->getSorting()) ? $criteria->getSorting()[0] : null; if ($sorting) { $this->isScoreSorting = $sorting->getField() === '_score'; } return $this->isScoreSorting; } /** * @param Criteria $criteria */ protected function addProductNumbersToCriteria(Criteria $criteria): void { if ($this->isAssocArray($this->doofinderIds)) { $criteria->addFilter( new OrFilter([ new EqualsAnyFilter('productNumber', array_keys($this->doofinderIds)), new EqualsAnyFilter('parent.productNumber', array_values($this->doofinderIds)), new EqualsAnyFilter('productNumber', array_values($this->doofinderIds)) ]) ); } else { $criteria->addFilter(new EqualsAnyFilter('productNumber', array_values($this->doofinderIds))); } } /** * @param array $arr * @return bool */ protected function isAssocArray(array $arr) { if (array() === $arr) return false; return array_keys($arr) !== range(0, count($arr) - 1); } /** * @param SearchPageLoadedEvent $event */ public function onSearchPageLoadedEvent(SearchPageLoadedEvent $event): void { $event->getPage()->setListing($this->modifyListing($event->getPage()->getListing())); } /** * @param SuggestPageLoadedEvent $event */ public function onSuggestPageLoadedEvent(SuggestPageLoadedEvent $event): void { $event->getPage()->setSearchResult($this->modifyListing($event->getPage()->getSearchResult())); } /** * @param EntitySearchResult $listing * @return object|ProductListingResult */ protected function modifyListing(EntitySearchResult $listing) { if ($listing && !empty($this->doofinderIds)) { // reorder entities if doofinder score sorting if ($this->isSuggestCall || $this->isScoreSorting) { $this->orderByProductNumberArray($listing->getEntities(), $listing->getContext()); } $newListing = ProductListingResult::createFrom(new EntitySearchResult( $listing->getEntity(), $listing->getTotal(), $this->sliceEntityCollection($listing->getEntities(), $this->shopwareOffset, $this->shopwareLimit), $listing->getAggregations(), $listing->getCriteria(), $listing->getContext() )); $newListing->setExtensions($listing->getExtensions()); $this->reintroduceShopwareLimitAndOffset($newListing); if ($this->isSuggestCall == false && $listing instanceof ProductListingResult) { $newListing->setSorting($listing->getSorting()); if (method_exists($listing, "getAvailableSortings") && method_exists($newListing, "setAvailableSortings")) { $newListing->setAvailableSortings($listing->getAvailableSortings()); } else if (method_exists($listing, "getSortings") && method_exists($newListing, "setSortings")) { $newListing->setSortings($listing->getSortings()); } } return $newListing; } return $listing; } /** * @param EntityCollection $collection * @param Context $context * @return EntityCollection */ protected function orderByProductNumberArray(EntityCollection $collection, Context $context): EntityCollection { if ($collection) { $productNumbers = array_keys($this->doofinderIds); $groupNumbers = array_values($this->doofinderIds); $parentIds = $collection->filter(function(ProductEntity $product) { return !!$product->getParentId(); })->map(function(ProductEntity $product) { return $product->getParentId(); }); $parentNumbers = $this->getParentNumbers($parentIds, $context); $collection->sort( function (ProductEntity $a, ProductEntity $b) use ($productNumbers, $groupNumbers, $parentNumbers) { $aIndex = array_search($a->getProductNumber(), $productNumbers); $bIndex = array_search($b->getProductNumber(), $productNumbers); // order by product number and search parents if (($aIndex === false || $bIndex === false) && ($parentNumbers[$a->getParentId()] || $parentNumbers[$b->getParentId()])) { $aIndex = array_search($parentNumbers[$a->getParentId()], $productNumbers); $bIndex = array_search($parentNumbers[$b->getParentId()], $productNumbers); } // order by group number and search parents if (($aIndex === false || $bIndex === false) && ($parentNumbers[$a->getParentId()] || $parentNumbers[$b->getParentId()])) { $aIndex = array_search($parentNumbers[$a->getParentId()], $groupNumbers); $bIndex = array_search($parentNumbers[$b->getParentId()], $groupNumbers); } return ($aIndex !== false ? $aIndex : PHP_INT_MAX) - ($bIndex !== false ? $bIndex : PHP_INT_MAX); } ); } return $collection; } /** * @param array $parentIds * @param Context $context * @return array */ protected function getParentNumbers(array $parentIds, Context $context): array { if (empty($parentIds)) { return []; } $parentNumbers = []; /** @var ProductEntity $parent */ foreach ($this->productRepository->search(new Criteria($parentIds), $context) as $parent) { $parentNumbers[$parent->getId()] = $parent->getProductNumber(); } return $parentNumbers; } /** * @param Criteria $criteria */ protected function storeShopwareLimitAndOffset(Criteria $criteria): void { $this->shopwareLimit = $criteria->getLimit(); $this->shopwareOffset = $criteria->getOffset(); } /** * @param Criteria $criteria */ protected function manipulateCriteriaLimitAndOffset(Criteria $criteria): void { $criteria->setLimit(count($this->doofinderIds)); $criteria->setOffset(0); } /** * @param ProductListingResult $newListing */ protected function reintroduceShopwareLimitAndOffset(ProductListingResult $newListing): void { $newListing->setLimit($this->shopwareLimit); $newListing->getCriteria()->setLimit($this->shopwareLimit); $newListing->getCriteria()->setOffset($this->shopwareOffset); } /** * @param EntityCollection $collection * @param $offset * @param $limit * @return EntityCollection */ protected function sliceEntityCollection(EntityCollection $collection, $offset, $limit): EntityCollection { $iterator = $collection->getIterator(); $newEntities = []; $i = 0; for ($iterator->rewind(); $iterator->valid(); $iterator->next()) { if ($i >= $offset && $i < $offset + $limit) { $newEntities[] = $iterator->current(); } $i++; } return new EntityCollection($newEntities); } /** * @param SalesChannelContext $context * @return array|bool|float|int|string|null */ protected function getDoofinderSearchSubscriberActivationMode(SalesChannelContext $context) { $doofinderSearchSubscriberActivate = $this->systemConfigService->get( 'IntediaDoofinderSW6.config.doofinderSearchSubscriberActivate', $context ? $context->getSalesChannel()->getId() : null ); return $doofinderSearchSubscriberActivate; }}