vendor/store.shopware.com/acrisfiltercs/src/Storefront/Subscriber/FilterSubscriber.php line 125

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