custom/plugins/MoorlFoundation/src/Core/System/EntityListingFeaturesSubscriberExtension.php line 95

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace MoorlFoundation\Core\System;
  3. use MoorlFoundation\Core\Content\Cms\SalesChannel\Struct\LocationStruct;
  4. use MoorlFoundation\Core\Content\Location\LocationDefinition;
  5. use MoorlFoundation\Core\Content\Location\LocationEntity;
  6. use MoorlFoundation\Core\Content\Sorting\SortingCollection;
  7. use MoorlFoundation\Core\Service\LocationService;
  8. use MoorlFoundation\Core\Service\LocationServiceV2;
  9. use MoorlFoundation\Core\Service\SortingService;
  10. use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
  11. use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
  12. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  13. use Shopware\Core\Content\Product\SalesChannel\Listing\FilterCollection;
  14. use Shopware\Core\Content\Product\SalesChannel\Sorting\ProductSortingEntity;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\StatsAggregation;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\PrefixFilter;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  25. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  26. use Symfony\Component\HttpFoundation\Request;
  27. class EntityListingFeaturesSubscriberExtension
  28. {
  29.     public const DEFAULT_SEARCH_SORT 'standard';
  30.     protected SortingService $sortingService;
  31.     protected ?LocationService $locationService null;
  32.     protected ?LocationServiceV2 $locationServiceV2 null;
  33.     protected string $entityName "";
  34.     public function __construct(
  35.         SortingService $sortingService,
  36.         ?LocationServiceV2 $locationServiceV2 null
  37.     )
  38.     {
  39.         $this->sortingService $sortingService;
  40.         $this->locationServiceV2 $locationServiceV2;
  41.     }
  42.     public function handleFlags(ProductListingCriteriaEvent $event): void
  43.     {
  44.         $request $event->getRequest();
  45.         $criteria $event->getCriteria();
  46.         if ($request->query->get('no-aggregations')) {
  47.             $criteria->resetAggregations();
  48.         }
  49.         if ($request->query->get('only-aggregations')) {
  50.             $criteria->setLimit(0);
  51.             $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
  52.             $criteria->resetSorting();
  53.             $criteria->resetAssociations();
  54.         }
  55.     }
  56.     public function handleListingRequest(ProductListingCriteriaEvent $event): void
  57.     {
  58.         $request $event->getRequest();
  59.         $criteria $event->getCriteria();
  60.         $context $event->getSalesChannelContext();
  61.         if (!$request->query->get('order')) {
  62.             $request->request->set('order'self::DEFAULT_SEARCH_SORT);
  63.         }
  64.         $this->handlePagination($request$criteria$event->getSalesChannelContext());
  65.         $this->handleFilters($request$criteria$context);
  66.         $this->handleSorting($request$criteria$context);
  67.     }
  68.     public function handleSearchRequest(ProductListingCriteriaEvent $event): void
  69.     {
  70.         $request $event->getRequest();
  71.         $criteria $event->getCriteria();
  72.         $context $event->getSalesChannelContext();
  73.         if (!$request->query->get('order')) {
  74.             $request->request->set('order'self::DEFAULT_SEARCH_SORT);
  75.         }
  76.         $this->handlePagination($request$criteria$event->getSalesChannelContext());
  77.         $this->handleFilters($request$criteria$context);
  78.         $this->handleSorting($request$criteria$context);
  79.     }
  80.     public function handleResult(ProductListingResultEvent $event): void
  81.     {
  82.         $this->addCurrentFilters($event);
  83.         $result $event->getResult();
  84.         $sortings $this->sortingService->getAvailableSortings(
  85.             $this->entityName,
  86.             $event->getSalesChannelContext()->getContext()
  87.         );
  88.         $currentSortingKey $this->getCurrentSorting($sortings$event->getRequest())->getKey();
  89.         $result->setSorting($currentSortingKey);
  90.         $result->setAvailableSortings($sortings);
  91.         $result->setPage($this->getPage($event->getRequest()));
  92.         $result->setLimit($this->getLimit($event->getRequest()));
  93.     }
  94.     private function handleFilters(Request $requestCriteria $criteriaSalesChannelContext $context): void
  95.     {
  96.         $filters $this->getFilters($request$context);
  97.         $aggregations $this->getAggregations($request$filters);
  98.         foreach ($aggregations as $aggregation) {
  99.             $criteria->addAggregation($aggregation);
  100.         }
  101.         if ($request->query->get('search')) {
  102.             $criteria->setTerm($request->query->get('search'));
  103.         }
  104.         foreach ($filters as $filter) {
  105.             if ($filter->isFiltered()) {
  106.                 $criteria->addPostFilter($filter->getFilter());
  107.             }
  108.         }
  109.         /* Handle "locationCache" if radius filter active */
  110.         if ($criteria->hasAssociation('locationCache') && $context->hasExtension(LocationDefinition::ENTITY_NAME)) {
  111.             /** @var LocationEntity $location */
  112.             $location $context->getExtension(LocationDefinition::ENTITY_NAME);
  113.             $locationCacheCriteria $criteria->getAssociation('locationCache');
  114.             $locationCacheCriteria->addFilter(new EqualsFilter('locationId'$location->getId()));
  115.             $context->removeExtension(LocationDefinition::ENTITY_NAME);
  116.             $request->attributes->set('order''distance');
  117.         } else if ($criteria->hasAssociation('locationCache')) {
  118.             $criteria->removeAssociation('locationCache');
  119.             $request->attributes->set('order''standard');
  120.         }
  121.         $criteria->addExtension('filters'$filters);
  122.     }
  123.     private function getAggregations(Request $requestFilterCollection $filters): array
  124.     {
  125.         $aggregations = [];
  126.         if ($request->query->get('reduce-aggregations') === null) {
  127.             foreach ($filters as $filter) {
  128.                 $aggregations array_merge($aggregations$filter->getAggregations());
  129.             }
  130.             return $aggregations;
  131.         }
  132.         foreach ($filters as $filter) {
  133.             $excluded $filters->filtered();
  134.             if ($filter->exclude()) {
  135.                 $excluded $excluded->blacklist($filter->getName());
  136.             }
  137.             foreach ($filter->getAggregations() as $aggregation) {
  138.                 if ($aggregation instanceof FilterAggregation) {
  139.                     $aggregation->addFilters($excluded->getFilters());
  140.                     $aggregations[] = $aggregation;
  141.                     continue;
  142.                 }
  143.                 $aggregation = new FilterAggregation(
  144.                     $aggregation->getName(),
  145.                     $aggregation,
  146.                     $excluded->getFilters()
  147.                 );
  148.                 $aggregations[] = $aggregation;
  149.             }
  150.         }
  151.         return $aggregations;
  152.     }
  153.     private function handlePagination(Request $requestCriteria $criteriaSalesChannelContext $context): void
  154.     {
  155.         $limit $this->getLimit($request);
  156.         $page $this->getPage($request);
  157.         $criteria->setOffset(($page 1) * $limit);
  158.         $criteria->setLimit($limit);
  159.         $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT);
  160.     }
  161.     private function handleSorting(Request $requestCriteria $criteriaSalesChannelContext $context): void
  162.     {
  163.         if ($criteria->getSorting()) {
  164.             return;
  165.         }
  166.         $sortings $this->sortingService->getAvailableSortings(
  167.             $this->entityName,
  168.             $context->getContext()
  169.         );
  170.         $currentSorting $this->getCurrentSorting($sortings$request);
  171.         $criteria->addSorting(...$currentSorting->createDalSorting());
  172.         $criteria->addExtension('sortings'$sortings);
  173.     }
  174.     private function getCurrentSorting(SortingCollection $sortingsRequest $request): ProductSortingEntity
  175.     {
  176.         $key $request->query->get('order');
  177.         $sorting $sortings->getByKey($key);
  178.         if ($sorting !== null) {
  179.             return $sorting;
  180.         }
  181.         return $sortings->first();
  182.     }
  183.     private function addCurrentFilters(ProductListingResultEvent $event): void
  184.     {
  185.         $result $event->getResult();
  186.         $filters $result->getCriteria()->getExtension('filters');
  187.         if (!$filters instanceof FilterCollection) {
  188.             return;
  189.         }
  190.         foreach ($filters as $filter) {
  191.             /* Handle "my location" if radius filter active */
  192.             if ($filter->getName() === 'radius') {
  193.                 $values $filter->getValues();
  194.                 if (isset($values['locationLat'])) {
  195.                     $me = new LocationStruct();
  196.                     $me->setLocationLat((float) $values['locationLat']);
  197.                     $me->setLocationLon((float) $values['locationLon']);
  198.                     $me->setLocationDistanceUnit((string) $values['unit']);
  199.                     $me->__set('locationDistance', (float) $values['distance']);
  200.                     $result->addExtension('me'$me);
  201.                 }
  202.             }
  203.             $result->addCurrentFilter($filter->getName(), $filter->getValues());
  204.         }
  205.     }
  206.     private function getLimit(Request $request): int
  207.     {
  208.         $limit $request->query->getInt('limit'0);
  209.         if ($request->isMethod(Request::METHOD_POST)) {
  210.             $limit $request->request->getInt('limit'$limit);
  211.         }
  212.         return $limit <= 12 $limit;
  213.     }
  214.     private function getPage(Request $request): int
  215.     {
  216.         $page $request->query->getInt('p'1);
  217.         if ($request->isMethod(Request::METHOD_POST)) {
  218.             $page $request->request->getInt('p'$page);
  219.         }
  220.         return $page <= $page;
  221.     }
  222.     protected function getFilters(Request $requestSalesChannelContext $context): FilterCollection
  223.     {
  224.         return new FilterCollection();
  225.     }
  226.     protected function getTagFilter(Request $request): Filter
  227.     {
  228.         $ids $this->getPropIds($request"tag");
  229.         return new Filter(
  230.             'tag',
  231.             !empty($ids),
  232.             [new EntityAggregation('tag'$this->entityName '.tags.id''tag')],
  233.             new EqualsAnyFilter($this->entityName '.tags.id'$ids),
  234.             $ids
  235.         );
  236.     }
  237.     protected function getManufacturerFilter(Request $request): Filter
  238.     {
  239.         $ids $this->getPropIds($request"manufacturer");
  240.         return new Filter(
  241.             'manufacturer',
  242.             !empty($ids),
  243.             [new EntityAggregation('manufacturer'$this->entityName '.productManufacturers.id''product_manufacturer')],
  244.             new EqualsAnyFilter($this->entityName '.productManufacturers.id'$ids),
  245.             $ids
  246.         );
  247.     }
  248.     protected function getCountryFilter(Request $request): Filter
  249.     {
  250.         $ids $this->getPropIds($request"country");
  251.         return new Filter(
  252.             'country',
  253.             !empty($ids),
  254.             [new EntityAggregation('country'$this->entityName '.countryId''country')],
  255.             new EqualsAnyFilter($this->entityName '.countryId'$ids),
  256.             $ids
  257.         );
  258.     }
  259.     protected function getTypeFilter(Request $request): Filter
  260.     {
  261.         $ids $this->getPropIds($request"type");
  262.         return new Filter(
  263.             'type',
  264.             !empty($ids),
  265.             [new EntityAggregation('type'$this->entityName '.typeId'$this->entityName '_type')],
  266.             new EqualsAnyFilter($this->entityName '.typeId'$ids),
  267.             $ids
  268.         );
  269.     }
  270.     protected function getCustomerFilter(Request $request): Filter
  271.     {
  272.         $ids $this->getPropIds($request"customer");
  273.         return new Filter(
  274.             'appflix-ad-customer',
  275.             !empty($ids),
  276.             [],
  277.             new EqualsAnyFilter($this->entityName '.customerId'$ids),
  278.             $ids
  279.         );
  280.     }
  281.     protected function getFirstCharFilter(Request $request): Filter
  282.     {
  283.         $firstChars $this->getPropIds($request"first-char");
  284.         $filter = [];
  285.         if ($firstChars) {
  286.             foreach ($firstChars as $firstChar) {
  287.                 $filter[] = new PrefixFilter('name'$firstChar);
  288.             }
  289.         }
  290.         return new Filter(
  291.             'first-char',
  292.             !empty($firstChar),
  293.             [new EntityAggregation('first-char'$this->entityName '.id'$this->entityName)],
  294.             new MultiFilter(MultiFilter::CONNECTION_OR$filter),
  295.             [
  296.                 'firstChars' => $firstChars
  297.             ]
  298.         );
  299.     }
  300.     protected function getPriceFilter(Request $request): Filter
  301.     {
  302.         $min $request->query->get('min-price'0);
  303.         $max $request->query->get('max-price'0);
  304.         $range = [];
  305.         if ($min 0) {
  306.             $range[RangeFilter::GTE] = $min;
  307.         }
  308.         if ($max 0) {
  309.             $range[RangeFilter::LTE] = $max;
  310.         }
  311.         return new Filter(
  312.             'price',
  313.             !empty($range),
  314.             [new StatsAggregation('price'$this->entityName '.price'truetruefalsefalse)],
  315.             new RangeFilter($this->entityName .'.price'$range),
  316.             [
  317.                 'min' => (float) $request->query->get('min-price'),
  318.                 'max' => (float) $request->query->get('max-price'),
  319.             ]
  320.         );
  321.     }
  322.     protected function getRadiusFilter(Request $request, ?SalesChannelContext $context null): Filter
  323.     {
  324.         /**
  325.          * km = Kilometer
  326.          * mi = Meilen
  327.          * nm = Nautische Meilen
  328.          */
  329.         $location $request->query->get('location''');
  330.         $distance $request->query->get('distance'0);
  331.         $unit $this->locationServiceV2->getUnitOfMeasurement();
  332.         $filter = new EqualsFilter($this->entityName '.active'true);
  333.         $location $this->locationServiceV2->getLocationByTerm($location$this->getPropIds($request"country"));
  334.         /* If a location was found, write locationCache, add filter and add location to salesChannelContext */
  335.         if ($location) {
  336.             $this->locationServiceV2->writeLocationCache($location$this->entityName, (float) $distance$unit);
  337.             $filter = new MultiFilter(MultiFilter::CONNECTION_AND, [
  338.                 new RangeFilter($this->entityName '.locationCache.distance', [RangeFilter::LTE => $distance]),
  339.                 new EqualsFilter($this->entityName '.locationCache.locationId'$location->getId())
  340.             ]);
  341.             $context->addExtension(LocationDefinition::ENTITY_NAME$location);
  342.         }
  343.         return new Filter(
  344.             'radius',
  345.             !empty($location),
  346.             [new CountAggregation('radius'$this->entityName '.active')],
  347.             $filter,
  348.             [
  349.                 'locationId' => !empty($location) ? $location->getId() : null,
  350.                 'distance' => (int) $distance,
  351.                 'unit' => $unit,
  352.                 'locationLat' => !empty($location) ? $location->getLocationLat() : null,
  353.                 'locationLon' => !empty($location) ? $location->getLocationLon() : null
  354.             ]
  355.         );
  356.     }
  357.     protected function getPropIds(Request $requeststring $prop "tag"): array
  358.     {
  359.         $ids $request->query->get($prop'');
  360.         if ($request->isMethod(Request::METHOD_POST)) {
  361.             $ids $request->request->get($prop'');
  362.         }
  363.         if (\is_string($ids)) {
  364.             $ids explode('|'$ids);
  365.         }
  366.         return array_filter((array) $ids);
  367.     }
  368. }