<?php declare(strict_types=1);
namespace Intedia\Doofinder\Storefront\Subscriber;
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\DataAbstractionLayer\EntityCollection;
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\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Page\Search\SearchPageLoadedEvent;
use Shopware\Storefront\Page\Suggest\SuggestPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class SearchSubscriber implements EventSubscriberInterface
{
/** @var SystemConfigService */
protected $systemConfigService;
/** @var LoggerInterface */
protected $logger;
/** @var Search */
protected $searchApi;
/** @var array */
protected $doofinderIds;
/** @var integer */
protected $doofinderTotal;
/** @var integer */
protected $doofinderOffset;
/** @var integer */
protected $shopwareLimit;
/** @var integer */
protected $shopwareOffset;
/** @var bool */
protected $isScoreSorting;
/** @var bool */
protected $isSuggestCall = false;
/**
* SearchSubscriber constructor.
* @param SystemConfigService $systemConfigService
* @param LoggerInterface $logger
* @param Search $searchApi
*/
public function __construct(SystemConfigService $systemConfigService, LoggerInterface $logger, Search $searchApi)
{
$this->systemConfigService = $systemConfigService;
$this->logger = $logger;
$this->searchApi = $searchApi;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
ProductSearchCriteriaEvent::class => 'onSearchCriteriaEvent',
SearchPageLoadedEvent::class => 'onSearchPageLoadedEvent',
ProductSuggestCriteriaEvent::class => 'onSuggestCriteriaEvent',
SuggestPageLoadedEvent::class => 'onSuggestPageLoadedEvent'
];
}
/**
* @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
{
if ($this->systemConfigService->get('IntediaDoofinderSW6.config.doofinderEnabled', $context ? $context->getSalesChannel()->getId() : null)) {
$term = $request->query->get('search');
if ($term) {
$this->doofinderIds = $this->searchApi->queryIds($term, $context);
$this->storeShopwareLimitAndOffset($criteria);
$this->manipulateCriteriaLimitAndOffset($criteria);
if (!empty($this->doofinderIds)) {
$this->resetCriteriaFiltersQueriesAndSorting($criteria);
$this->addProductNumbersToCriteria($criteria);
}
}
}
}
/**
* @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
{
$criteria->addFilter(
new OrFilter([
new EqualsAnyFilter('productNumber', array_values($this->doofinderIds)),
new EqualsAnyFilter('parentId', array_keys($this->doofinderIds)),
new EqualsAnyFilter('id', array_keys($this->doofinderIds))
])
);
}
/**
* @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());
}
$newListing = ProductListingResult::createFrom(new EntitySearchResult(
$listing->getEntity(),
$listing->getTotal(),
$this->sliceEntityCollection($listing->getEntities(), $this->shopwareOffset, $this->shopwareLimit),
$listing->getAggregations(),
$listing->getCriteria(),
$listing->getContext()
));
$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
* @return EntityCollection
*/
protected function orderByProductNumberArray(EntityCollection $collection): EntityCollection
{
if ($collection) {
$sortingNumbers = array_keys($this->doofinderIds);
$fallbackNumbers = array_values($this->doofinderIds);
$collection->sort(
function (ProductEntity $a, ProductEntity $b) use ($sortingNumbers, $fallbackNumbers) {
$aIndex = false;
$bIndex = false;
if ($a->getParentId() || $b->getParentId()) {
$aIndex = array_search($a->getParentId(), $sortingNumbers);
$bIndex = array_search($b->getParentId(), $sortingNumbers);
}
if ($aIndex === false || $bIndex === false) {
$aIndex = $aIndex !== false ? $aIndex : array_search($a->getId(), $sortingNumbers);
$bIndex = $bIndex !== false ? $bIndex : array_search($b->getId(), $sortingNumbers);
}
if ($aIndex === false || $bIndex === false) {
$aIndex = $aIndex !== false ? $aIndex : array_search($a->getProductNumber(), $fallbackNumbers);
$bIndex = $bIndex !== false ? $bIndex : array_search($b->getProductNumber(), $fallbackNumbers);
}
return ($aIndex !== false ? $aIndex : PHP_INT_MAX) - ($bIndex !== false ? $bIndex : PHP_INT_MAX); }
);
}
return $collection;
}
/**
* @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);
}
}