custom/plugins/AcrisFilterCS/src/Storefront/Subscriber/FilterSubscriber.php line 105

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Acris\Filter\Storefront\Subscriber;
  3. use Acris\Filter\Custom\FilterEntity;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Content\Category\CategoryCollection;
  6. use Shopware\Core\Content\Category\CategoryEntity;
  7. use Shopware\Core\Content\Category\Tree\Tree;
  8. use Shopware\Core\Content\Category\Tree\TreeItem;
  9. use Shopware\Core\Content\Product\Events\ProductListingCollectFilterEvent;
  10. use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
  11. use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
  12. use Shopware\Core\Content\Product\Events\ProductSearchResultEvent;
  13. use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
  14. use Shopware\Core\Content\Product\Events\ProductSuggestRouteCacheKeyEvent;
  15. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  16. use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult;
  17. use Shopware\Core\Framework\Context;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\EntityResult;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  30. use Shopware\Core\Framework\Struct\ArrayEntity;
  31. use Shopware\Core\Framework\Uuid\Uuid;
  32. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  33. use Shopware\Core\System\SystemConfig\SystemConfigService;
  34. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  35. use Symfony\Component\HttpFoundation\Request;
  36. class FilterSubscriber implements EventSubscriberInterface
  37. {
  38.     private const CATEGORY_FILTER_AGGREGATION_TREE 'acrisCategoryFilterAggregationTree';
  39.     /**
  40.      * @var Connection
  41.      */
  42.     private $connection;
  43.     /**
  44.      * @var EntityRepositoryInterface
  45.      */
  46.     private $optionRepository;
  47.     /**
  48.      * @var EntityRepositoryInterface
  49.      */
  50.     private $filterRepository;
  51.     /**
  52.      * @var SystemConfigService
  53.      */
  54.     private $systemConfigService;
  55.     /**
  56.      * @var null|array
  57.      */
  58.     private $filters;
  59.     /**
  60.      * @var TreeItem
  61.      */
  62.     private $treeItem;
  63.     public function __construct(Connection $connectionEntityRepositoryInterface $optionRepositoryEntityRepositoryInterface $filterRepositorySystemConfigService $systemConfigService)
  64.     {
  65.         $this->connection $connection;
  66.         $this->optionRepository $optionRepository;
  67.         $this->filterRepository $filterRepository;
  68.         $this->systemConfigService $systemConfigService;
  69.         $this->filters null;
  70.         $this->treeItem = new TreeItem(null, []);
  71.     }
  72.     public static function getSubscribedEvents(): array
  73.     {
  74.         return [
  75.             ProductListingResultEvent::class => 'onProductListingSearchResultEvent',
  76.             ProductSearchResultEvent::class => 'onProductListingSearchResultEvent',
  77.             ProductListingCollectFilterEvent::class => 'onProductListingCollectFilters',
  78.             ProductSearchCriteriaEvent::class => [
  79.                 ['onProductSearchCriteria', -100],
  80.             ],
  81.             ProductSuggestCriteriaEvent::class => [
  82.                 ['onProductSuggestCriteria', -100],
  83.             ],
  84.             ProductSuggestRouteCacheKeyEvent::class => [
  85.                 ['onProductSuggestRouteCacheKeyEvent', -200],
  86.             ]
  87.         ];
  88.     }
  89.     public function onProductSuggestRouteCacheKeyEvent(ProductSuggestRouteCacheKeyEvent $event): void
  90.     {
  91.         if (!empty($event->getRequest()) && !empty($event->getRequest()->get('categories'))) {
  92.             $event->setParts([...$event->getParts(), $event->getRequest()->get('categories')]);
  93.         }
  94.     }
  95.     public function onProductListingSearchResultEvent(ProductListingResultEvent $event): void
  96.     {
  97.         /** @var EntitySearchResult $filterResult */
  98.         $filterResult $this->filterRepository->search((new Criteria())->addSorting(new FieldSorting('position'FieldSorting::ASCENDING))->addFilter(new EqualsFilter('active'true)), $event->getContext());
  99.         $this->sortFilterResult($filterResult);
  100.         if ($filterResult->getTotal() > && $filterResult->first()) {
  101.             $event->getResult()->addExtension('acrisFilter', new ArrayEntity([
  102.                 'sortedFilters' => $filterResult->getEntities()->getElements()
  103.             ]));
  104.         }
  105.         $this->buildCategoryTree($event->getResult(), $event->getSalesChannelContext());
  106.     }
  107.     public function onProductListingCollectFilters(ProductListingCollectFilterEvent $event)
  108.     {
  109.         $acrisFilters $this->filterRepository->search(new Criteria(), $event->getContext())->getElements();
  110.         $filters $event->getFilters();
  111.         $request $event->getRequest();
  112.         $isSearchPage $this->isSearchPage($request);
  113.         /** @var FilterEntity $acrisFilter */
  114.         foreach ($acrisFilters as $acrisFilter) {
  115.             if ($acrisFilter->isActive() === true) {
  116.                 switch ($acrisFilter->getIdentifier()) {
  117.                     case 'properties':
  118.                         $propertiesFilter $filters->get('properties');
  119.                         if ($propertiesFilter instanceof Filter) {
  120.                             $filters->add($this->getPropertyRangeFilter($request$event->getContext(), $propertiesFilter));
  121.                         }
  122.                         break;
  123.                     case 'availability':
  124.                         $filters->add($this->getAvailabilityFilter($request));
  125.                         break;
  126.                     case 'categories':
  127.                         if ($isSearchPage === true) {
  128.                             $filters->add($this->getCategoriesFilter($request));
  129.                         }
  130.                         break;
  131.                 }
  132.             } else {
  133.                 if ($filters->has($acrisFilter->getIdentifier())) {
  134.                     $filters->remove($acrisFilter->getIdentifier());
  135.                 }
  136.             }
  137.         }
  138.     }
  139.     private function getPropertyRangeFilter(Request $requestContext $contextFilter $propertiesFilter): Filter
  140.     {
  141.         $min $request->get('min-property');
  142.         $max $request->get('max-property');
  143.         $ranges = [];
  144.         if (!$min && !$max) {
  145.             return $propertiesFilter;
  146.         }
  147.         if ($min !== NULL) {
  148.             foreach ($min as $minKey => $minValue) {
  149.                 $ranges[$minKey][RangeFilter::GTE] = floatval($minValue);
  150.             }
  151.         }
  152.         if ($max !== NULL) {
  153.             foreach ($max as $maxKey => $maxValue) {
  154.                 $ranges[$maxKey][RangeFilter::LTE] = floatval($maxValue);
  155.             }
  156.         }
  157.         $grouped = [];
  158.         $ids = [];
  159.         foreach ($ranges as $groupId => $range) {
  160.             $optionIds $this->optionRepository->searchIds(
  161.                 (new Criteria())->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  162.                         new RangeFilter('customFields.acris_filter_numeric'$ranges[$groupId]),
  163.                         new EqualsFilter('groupId'$groupId)
  164.                     ])
  165.                 ), $context)->getIds();
  166.             if (!empty($optionIds)) {
  167.                 $grouped[$groupId] = $optionIds;
  168.             } else {
  169.                 $grouped[$groupId] = [Uuid::randomHex()];
  170.             }
  171.             $ids array_merge($optionIds$ids);
  172.         }
  173.         // if no options were found no products should be shown
  174.         if (empty($ids) === true) {
  175.             $ids = [Uuid::randomHex()];
  176.         }
  177.         $ids array_merge($propertiesFilter->getValues(), $ids);
  178.         $filters = [$propertiesFilter->getFilter()];
  179.         foreach ($grouped as $optionIds) {
  180.             $filters[] = new MultiFilter(
  181.                 MultiFilter::CONNECTION_OR,
  182.                 [
  183.                     new EqualsAnyFilter('product.optionIds'$optionIds),
  184.                     new EqualsAnyFilter('product.propertyIds'$optionIds),
  185.                 ]
  186.             );
  187.         }
  188.         return new Filter(
  189.             'properties',
  190.             true,
  191.             $propertiesFilter->getAggregations(),
  192.             new MultiFilter(MultiFilter::CONNECTION_AND$filters),
  193.             $ids,
  194.             false
  195.         );
  196.     }
  197.     private function getAvailabilityFilter(Request $request): Filter
  198.     {
  199.         $filtered = (bool)$request->get('availability'false);
  200.         $ranges = [RangeFilter::GT => 0];
  201.         return new Filter(
  202.             'availability',
  203.             $filtered === true,
  204.             [
  205.                 new FilterAggregation(
  206.                     'availability-filter',
  207.                     new MaxAggregation('availability''product.availableStock'),
  208.                     [new RangeFilter('product.availableStock'$ranges)]
  209.                 ),
  210.             ],
  211.             new RangeFilter('product.availableStock'$ranges),
  212.             $filtered
  213.         );
  214.     }
  215.     public function onProductSearchCriteria(ProductSearchCriteriaEvent $event): void
  216.     {
  217.         $criteria $event->getCriteria();
  218.         if (empty($criteria) || empty($criteria->getTitle()) || $criteria->getTitle() !== "search-page" || empty($criteria->getAggregations())) return;
  219.         if (!$this->systemConfigService->get('AcrisFilterCS.config.loadPropertyFilterAtSearchPage'$event->getSalesChannelContext()->getSalesChannel()->getId())) return;
  220.         $aggregations $criteria->getAggregations();
  221.         if (is_array($aggregations) && array_key_exists('properties'$aggregations) && !empty($aggregations['properties'])) {
  222.             unset($aggregations['properties']);
  223.             $criteria->resetAggregations();
  224.             if (!empty($aggregations)) {
  225.                 foreach ($aggregations as $aggregation) {
  226.                     $criteria->addAggregation($aggregation);
  227.                 }
  228.             }
  229.         }
  230.     }
  231.     public function onProductSuggestCriteria(ProductSuggestCriteriaEvent $event): void
  232.     {
  233.         if (empty($event) || empty($event->getRequest()) || empty($event->getRequest()->query) || !$event->getRequest()->query->has('categories') || empty($event->getRequest()->query->get('categories'))) return;
  234.         $event->getCriteria()->addFilter(new EqualsFilter('categoriesRo.id'$event->getRequest()->query->get('categories')));
  235.     }
  236.     private function getCategoriesFilter(Request $request): Filter
  237.     {
  238.         $ids $request->query->get('categories''');
  239.         if ($request->isMethod(Request::METHOD_POST)) {
  240.             $ids $request->request->get('categories''');
  241.         }
  242.         if (!empty($ids) && is_string($ids)) {
  243.             $ids explode('|'$ids);
  244.         }
  245.         if (empty($ids) === true || is_array($ids) === false) {
  246.             $ids = [];
  247.         }
  248.         return new Filter(
  249.             'categories',
  250.             !empty($ids),
  251.             [new EntityAggregation('categories''product.categoriesRo.id''category')],
  252.             new EqualsAnyFilter('product.categoriesRo.id'$ids),
  253.             $ids
  254.         );
  255.     }
  256.     private function isSearchPage(Request $request): bool
  257.     {
  258.         return $request->attributes->get('_route') === 'frontend.search.page' || $request->attributes->get('_route') === 'widgets.search.filter' || $request->attributes->get('_route') === 'widgets.search.pagelet.v2';
  259.     }
  260.     private function buildCategoryTree(ProductListingResult $productListingResultSalesChannelContext $context): void
  261.     {
  262.         $categoryAggregation $productListingResult->getAggregations()->get('categories');
  263.         if (!$categoryAggregation instanceof EntityResult || $categoryAggregation->getEntities()->count() === 0) {
  264.             return;
  265.         }
  266.         $categoryCollection $categoryAggregation->getEntities();
  267.         if (!$categoryCollection instanceof CategoryCollection) {
  268.             return;
  269.         }
  270.         $clonedCategoryConnection = clone $categoryCollection;
  271.         $tree $this->loadTree(null$clonedCategoryConnection$context);
  272.         $categoryAggregation->addExtension(self::CATEGORY_FILTER_AGGREGATION_TREE, new Tree(null$tree));
  273.     }
  274.     /**
  275.      * Copied and modified from Core/Content/Category/Service/NavigationLoader.php
  276.      *
  277.      * @param CategoryEntity[] $categories
  278.      *
  279.      * @return TreeItem[]
  280.      */
  281.     private function buildTree(?string $parentIdCategoryCollection $categories): array
  282.     {
  283.         $children = new CategoryCollection();
  284.         foreach ($categories->getElements() as $category) {
  285.             if ($category->getParentId() !== $parentId) {
  286.                 continue;
  287.             }
  288.             $categories->remove($category->getId());
  289.             $children->add($category);
  290.         }
  291.         $children->sortByPosition();
  292.         $items = [];
  293.         foreach ($children as $child) {
  294.             if (!$child->getActive() || !$child->getVisible()) {
  295.                 continue;
  296.             }
  297.             $item = clone $this->treeItem;
  298.             $item->setCategory($child);
  299.             $item->setChildren(
  300.                 $this->buildTree($child->getId(), $categories)
  301.             );
  302.             $items[$child->getId()] = $item;
  303.         }
  304.         return $items;
  305.     }
  306.     private function loadTree(?string $parentIdCategoryCollection $categoriesSalesChannelContext $context): array
  307.     {
  308.         $tree $this->buildTree($parentId$categories);
  309.         if (!empty($tree)) {
  310.             foreach ($tree as $key => $category) {
  311.                 if ($key !== $context->getSalesChannel()->getNavigationCategoryId()) unset($tree[$key]);
  312.             }
  313.         }
  314.         return $tree;
  315.     }
  316.     private function sortFilterResult(EntitySearchResult $filterResult): void
  317.     {
  318.         $filterResult->getEntities()->sort(function (FilterEntity $aFilterEntity $b) {
  319.             return $a->getPosition() <=> $b->getPosition();
  320.         });
  321.     }
  322. }