custom/plugins/MaxiaVariantsTable6/src/Storefront/Subscriber/VariantsTableFilterSubscriber.php line 76

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Maxia\MaxiaVariantsTable6\Storefront\Subscriber;
  3. use Doctrine\DBAL\Connection;
  4. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Column\DataMapping\ProductOptionMapping;
  5. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Column\DataMapping\ProductOptionsMapping;
  6. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Column\DataMapping\ProductPropertiesMapping;
  7. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Column\DataMapping\ProductPropertyMapping;
  8. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Events\VariantsTableCollectFilterEvent;
  9. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\Events\VariantsTableSearchCriteriaEvent;
  10. use Maxia\MaxiaVariantsTable6\Core\Content\VariantsTable\VariantsTableEntity;
  11. use Maxia\MaxiaVariantsTable6\Storefront\Pagelet\VariantsTable\Events\VariantsTablePageletLoadedEvent;
  12. use Maxia\MaxiaVariantsTable6\Storefront\Pagelet\VariantsTable\VariantsTablePagelet;
  13. use Shopware\Core\Content\Product\ProductEntity;
  14. use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
  15. use Shopware\Core\Content\Product\SalesChannel\Listing\FilterCollection;
  16. use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingFeaturesSubscriber;
  17. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
  18. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionEntity;
  19. use Shopware\Core\Content\Property\PropertyGroupEntity;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  22. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MinAggregation;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\TermsResult;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\EntityResult;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  34. use Shopware\Core\Framework\Uuid\Uuid;
  35. use Shopware\Core\PlatformRequest;
  36. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  37. use Shopware\Core\System\SystemConfig\SystemConfigService;
  38. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  39. use Symfony\Component\HttpFoundation\Request;
  40. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  41. /**
  42.  * Extends the search criteria and loads filter values
  43.  *
  44.  * @package Maxia\MaxiaVariantsTable6\Subscriber
  45.  */
  46. class VariantsTableFilterSubscriber implements EventSubscriberInterface
  47. {
  48.     public static function getSubscribedEvents(): array
  49.     {
  50.         return [
  51.             VariantsTableSearchCriteriaEvent::class => 'handleSearchCriteria',
  52.             VariantsTablePageletLoadedEvent::class => 'handleResult'
  53.         ];
  54.     }
  55.     private SystemConfigService $systemConfig;
  56.     private EntityRepositoryInterface $optionRepository;
  57.     private Connection $connection;
  58.     private EventDispatcherInterface $dispatcher;
  59.     public function __construct(
  60.         SystemConfigService $systemConfig,
  61.         EntityRepositoryInterface $optionRepository,
  62.         Connection $connection,
  63.         EventDispatcherInterface $dispatcher)
  64.     {
  65.         $this->systemConfig $systemConfig;
  66.         $this->optionRepository $optionRepository;
  67.         $this->connection $connection;
  68.         $this->dispatcher $dispatcher;
  69.     }
  70.     public function handleSearchCriteria(VariantsTableSearchCriteriaEvent $event): void
  71.     {
  72.         $request $event->getRequest();
  73.         $criteria $event->getCriteria();
  74.         $context $event->getSalesChannelContext();
  75.         $criteria->addAssociation('options');
  76.         $this->handleFilters($request$event->getVariantsTableEntity(), $criteria$context);
  77.     }
  78.     public function handleResult(VariantsTablePageletLoadedEvent $event): void
  79.     {
  80.         if (!$event->getPagelet()->getConfig()->isShowFilters()) {
  81.             return;
  82.         }
  83.         $this->addCurrentFilters($event);
  84.         if ($event->getRequest()->attributes->get('_route') === 'frontend.plugins.maxia-variants-table.ajax') {
  85.             return;
  86.         }
  87.         $this->groupOptionAggregations($event);
  88.     }
  89.     private function handleFilters(Request $requestVariantsTableEntity $variantsTableCriteria $criteriaSalesChannelContext $context): void
  90.     {
  91.         $filters $this->getFilters($request$variantsTable$context);
  92.         $aggregations $this->getAggregations($request$filters);
  93.         foreach ($aggregations as $aggregation) {
  94.             $criteria->addAggregation($aggregation);
  95.         }
  96.         foreach ($filters as $filter) {
  97.             if ($filter->isFiltered()) {
  98.                 $criteria->addFilter($filter->getFilter());
  99.             }
  100.         }
  101.         $criteria->addExtension('filters'$filters);
  102.     }
  103.     private function getAggregations(Request $requestFilterCollection $filters): array
  104.     {
  105.         $aggregations = [];
  106.         if ($request->get('reduce-aggregations') === null) {
  107.             foreach ($filters as $filter) {
  108.                 $aggregations array_merge($aggregations$filter->getAggregations());
  109.             }
  110.             return $aggregations;
  111.         }
  112.         foreach ($filters as $filter) {
  113.             $excluded $filters->filtered();
  114.             if ($filter->exclude()) {
  115.                 $excluded $excluded->blacklist($filter->getName());
  116.             }
  117.             foreach ($filter->getAggregations() as $aggregation) {
  118.                 if ($aggregation instanceof FilterAggregation) {
  119.                     $aggregation->addFilters($excluded->getFilters());
  120.                     $aggregations[] = $aggregation;
  121.                     continue;
  122.                 }
  123.                 $aggregation = new FilterAggregation(
  124.                     $aggregation->getName(),
  125.                     $aggregation,
  126.                     $excluded->getFilters()
  127.                 );
  128.                 $aggregations[] = $aggregation;
  129.             }
  130.         }
  131.         return $aggregations;
  132.     }
  133.     private function collectOptionIds(VariantsTablePageletLoadedEvent $event): array
  134.     {
  135.         $aggregations $event->getPagelet()->getSearchResult()->getAggregations();
  136.         /** @var TermsResult|null $properties */
  137.         $properties $aggregations->get('properties');
  138.         /** @var TermsResult|null $options */
  139.         $options $aggregations->get('options');
  140.         $options $options $options->getKeys() : [];
  141.         $properties $properties $properties->getKeys() : [];
  142.         return array_unique(array_filter(array_merge($options$properties)));
  143.     }
  144.     /**
  145.      * Returns option IDs mapped by property_group IDs for all variant options of the product.
  146.      *
  147.      * @param ProductEntity $parentProduct
  148.      * @return array
  149.      * @throws \Doctrine\DBAL\Exception
  150.      */
  151.     private function getVariantPropertyIds(ProductEntity $parentProduct)
  152.     {
  153.         $results $this->connection->fetchAllAssociative("
  154.             SELECT 
  155.                 `property`.property_group_id, `property`.id
  156.             FROM 
  157.                 `product`
  158.             LEFT JOIN 
  159.                 `product_option` AS `option` ON `option`.product_id = `product`.id
  160.             LEFT JOIN 
  161.                 `property_group_option` `property` ON `property`.id = `option`.property_group_option_id
  162.             WHERE 
  163.                 `product`.parent_id = :parentId
  164.                 AND `option`.product_version_id = :versionId
  165.             GROUP BY `property`.id
  166.         ",
  167.             [
  168.                 'parentId' => Uuid::fromHexToBytes($parentProduct->getId()),
  169.                 'versionId' => Uuid::fromHexToBytes($parentProduct->getVersionId())
  170.             ],
  171.         );
  172.         $options = [];
  173.         foreach ($results as $result) {
  174.             $groupId Uuid::fromBytesToHex($result['property_group_id']);
  175.             $optionId Uuid::fromBytesToHex($result['id']);
  176.             if (!isset($options[$groupId])) {
  177.                 $options[$groupId] = [];
  178.             }
  179.             $options[$groupId][] = $optionId;
  180.         }
  181.         return $options;
  182.     }
  183.     private function groupOptionAggregations(VariantsTablePageletLoadedEvent $event): void
  184.     {
  185.         $ids $this->collectOptionIds($event);
  186.         if (empty($ids)) {
  187.             return;
  188.         }
  189.         $criteria = new Criteria($ids);
  190.         $criteria->setLimit(500);
  191.         $criteria->addAssociation('group');
  192.         $criteria->addAssociation('media');
  193.         $criteria->setTitle('product-listing::property-filter');
  194.         $criteria->addSorting(new FieldSorting('id'FieldSorting::ASCENDING));
  195.         $filterGroupIds $this->getFilterablePropertyGroupIds($event->getPagelet());
  196.         if ($filterGroupIds) {
  197.             $criteria->addFilter(new EqualsAnyFilter('group.id'$filterGroupIds));
  198.         }
  199.         $mergedOptions = new PropertyGroupOptionCollection();
  200.         $repositoryIterator = new RepositoryIterator($this->optionRepository$event->getContext(), $criteria);
  201.         while (($result $repositoryIterator->fetch()) !== null) {
  202.             $mergedOptions->merge($result->getEntities());
  203.         }
  204.         // filter out options that are not associated with any variant, if setting 'mergeVariantOptions' is disabled
  205.         $filterConfig $event->getPagelet()->getConfig()->getFilterConfig();
  206.         if ($filterConfig && isset($filterConfig['mergeVariantOptions']) && !$filterConfig['mergeVariantOptions']) {
  207.             $variantOptions $this->getVariantPropertyIds($event->getPagelet()->getParentProduct());
  208.             $mergedOptions $mergedOptions->filter(function (PropertyGroupOptionEntity $property) use ($variantOptions) {
  209.                 if (!isset($variantOptions[$property->getGroupId()])) {
  210.                     return false;
  211.                 }
  212.                 return in_array($property->getId(), $variantOptions[$property->getGroupId()]);
  213.             });
  214.         }
  215.         // group options by their property-group
  216.         $grouped $mergedOptions->groupByPropertyGroups();
  217.         $grouped->sortByPositions();
  218.         $grouped->sortByConfig();
  219.         $aggregations $event->getPagelet()->getSearchResult()->getAggregations();
  220.         // remove id results to prevent wrong usages
  221.         $aggregations->remove('properties');
  222.         $aggregations->remove('configurators');
  223.         $aggregations->remove('options');
  224.         $aggregations->add(new EntityResult('properties'$grouped));
  225.     }
  226.     private function addCurrentFilters(VariantsTablePageletLoadedEvent $event): void
  227.     {
  228.         $result $event->getPagelet()->getSearchResult();
  229.         $filters $result->getCriteria()->getExtension('filters');
  230.         if (!$filters instanceof FilterCollection) {
  231.             return;
  232.         }
  233.         foreach ($filters as $filter) {
  234.             $event->getPagelet()->getSearchResult()->addCurrentFilter($filter->getName(), $filter->getValues());
  235.         }
  236.     }
  237.     private function getPropertyIdsFromRequest(Request $request): array
  238.     {
  239.         $ids $request->query->get('properties''');
  240.         if ($request->isMethod(Request::METHOD_POST)) {
  241.             $ids $request->request->get('properties''');
  242.         }
  243.         if (\is_string($ids)) {
  244.             $ids explode('|'$ids);
  245.         }
  246.         return array_filter((array) $ids);
  247.     }
  248.     protected function getFilters(Request $requestVariantsTableEntity $variantsTableSalesChannelContext $context): FilterCollection
  249.     {
  250.         $filters = new FilterCollection();
  251.         $filters->add($this->getAvailableFilter($request$variantsTable));
  252.         if ($variantsTable->isShowFilters()) {
  253.             $filters->add($this->getPropertyFilter($request));
  254.             if (!$request->request->get('available-filter'true)) {
  255.                 $filters->remove('available-free');
  256.             }
  257.             if (!$request->request->get('property-filter'true)) {
  258.                 $filters->remove('properties');
  259.                 if (\count($propertyWhitelist $request->request->all(ProductListingFeaturesSubscriber::PROPERTY_GROUP_IDS_REQUEST_PARAM))) {
  260.                     $filters->add($this->getPropertyFilter($request$propertyWhitelist));
  261.                 }
  262.             }
  263.         }
  264.         $event = new VariantsTableCollectFilterEvent($request$filters$context);
  265.         $this->dispatcher->dispatch($event);
  266.         return $filters;
  267.     }
  268.     protected function getPropertyFilter(Request $request, ?array $groupIds null): Filter
  269.     {
  270.         $ids $this->getPropertyIdsFromRequest($request);
  271.         $propertyAggregation = new TermsAggregation('properties''product.properties.id');
  272.         $optionAggregation = new TermsAggregation('options''product.options.id');
  273.         if ($groupIds) {
  274.             $optionAggregation = new FilterAggregation(
  275.                 'options-filter',
  276.                 $optionAggregation,
  277.                 [new EqualsAnyFilter('product.options.groupId'$groupIds)]
  278.             );
  279.             $propertyAggregation = new FilterAggregation(
  280.                 'properties-filter',
  281.                 $propertyAggregation,
  282.                 [new EqualsAnyFilter('product.properties.groupId'$groupIds)]
  283.             );
  284.         }
  285.         if (empty($ids)) {
  286.             return new Filter(
  287.                 'properties',
  288.                 false,
  289.                 [$propertyAggregation$optionAggregation],
  290.                 new MultiFilter(MultiFilter::CONNECTION_OR, []),
  291.                 [],
  292.                 false
  293.             );
  294.         }
  295.         $grouped $this->connection->fetchAll(
  296.             'SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id
  297.              FROM property_group_option
  298.              WHERE id IN (:ids)',
  299.             ['ids' => Uuid::fromHexToBytesList($ids)],
  300.             ['ids' => Connection::PARAM_STR_ARRAY]
  301.         );
  302.         $grouped FetchModeHelper::group($grouped);
  303.         $filters = [];
  304.         foreach ($grouped as $options) {
  305.             $options array_column($options'id');
  306.             $filters[] = new MultiFilter(
  307.                 MultiFilter::CONNECTION_OR,
  308.                 [
  309.                     new EqualsAnyFilter('product.optionIds'$options),
  310.                     new EqualsAnyFilter('product.propertyIds'$options),
  311.                 ]
  312.             );
  313.         }
  314.         return new Filter(
  315.             'properties',
  316.             true,
  317.             [$propertyAggregation$optionAggregation],
  318.             new MultiFilter(MultiFilter::CONNECTION_AND$filters),
  319.             $ids,
  320.             false
  321.         );
  322.     }
  323.     protected function getAvailableFilter(Request $requestVariantsTableEntity $variantsTableEntity): Filter
  324.     {
  325.         /** @var SalesChannelContext $context */
  326.         $salesChannelContext $request->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  327.         $availableField 'product.available';
  328.         if ($this->systemConfig->get('DvsnStoreStock.config.status'$salesChannelContext->getSalesChannelId())) {
  329.             $translated $salesChannelContext->getSalesChannel()->getTranslated();
  330.             if (isset($translated['customFields'])
  331.                 && isset($translated['customFields']['dvsn_store_stock_sales_channel_status'])
  332.                 && $translated['customFields']['dvsn_store_stock_sales_channel_status'])
  333.             {
  334.                 $storeId $translated['customFields']['dvsn_store_stock_sales_channel_status_store'];
  335.                 $availableField 'customFields.dvsn_store_stock."' $storeId '".available';
  336.             }
  337.         }
  338.         return $this->createAvailableFilter($request$variantsTableEntity$availableField);
  339.     }
  340.     /**
  341.      * Creates the show or hide unavailable products filter, depending on the plugin setting "isShowUnavailable"
  342.      *
  343.      * @param Request $request
  344.      * @param VariantsTableEntity $variantsTableEntity
  345.      * @param string $field
  346.      * @return Filter
  347.      */
  348.     protected function createAvailableFilter(
  349.         Request $request,
  350.         VariantsTableEntity $variantsTableEntity,
  351.         string $field 'product.available'
  352.     ) {
  353.         $checked = (bool) $request->get('available'false);
  354.         if ($variantsTableEntity->isShowUnavailable()) {
  355.             return new Filter(
  356.                 'available',
  357.                 $checked,
  358.                 [
  359.                     new FilterAggregation(
  360.                         'available-filter',
  361.                         new MaxAggregation('available'$field),
  362.                         [new EqualsFilter($fieldtrue)]
  363.                     ),
  364.                 ],
  365.                 new EqualsFilter($fieldtrue),
  366.                 $checked
  367.             );
  368.         } else {
  369.             return new Filter(
  370.                 'available',
  371.                 !$checked,
  372.                 [
  373.                     new FilterAggregation(
  374.                         'available-filter',
  375.                         new MinAggregation('available'$field),
  376.                         [new EqualsFilter($fieldfalse)]
  377.                     ),
  378.                 ],
  379.                 new EqualsFilter($fieldtrue),
  380.                 $checked
  381.             );
  382.         }
  383.     }
  384.     /**
  385.      * Returns filterable property group ids by reading the filter config
  386.      *
  387.      * @param VariantsTablePagelet $pagelet
  388.      * @return array
  389.      */
  390.     protected function getFilterablePropertyGroupIds(VariantsTablePagelet $pagelet)
  391.     {
  392.         $filterGroupIds = [];
  393.         $filterConfig $pagelet->getConfig()->getFilterConfig();
  394.         if ($filterConfig['displayMode'] === 'selected-properties') {
  395.             if ($filterConfig['selectedProperties']) {
  396.                 $filterGroupIds $filterConfig['selectedProperties'];
  397.             } else {
  398.                 $filterConfig['displayMode'] = 'auto';
  399.                 $pagelet->getConfig()->setFilterConfig($filterConfig);
  400.             }
  401.         }
  402.         if ($filterConfig['displayMode'] === 'auto') {
  403.             $filterGroupIds $this->getPropertyGroupIdsFromTableColumns($pagelet);
  404.             if (!$filterGroupIds) {
  405.                 $filterConfig['displayMode'] = 'all';
  406.                 $pagelet->getConfig()->setFilterConfig($filterConfig);
  407.             }
  408.         }
  409.         return $filterGroupIds;
  410.     }
  411.     /**
  412.      * Returns property group ids for all columns in the table
  413.      *
  414.      * @param VariantsTablePagelet $pagelet
  415.      * @return array
  416.      */
  417.     protected function getPropertyGroupIdsFromTableColumns(VariantsTablePagelet $pagelet)
  418.     {
  419.         $groupIds = [];
  420.         foreach ($pagelet->getConfig()->getColumns() as $column) {
  421.             if ($column->getDataMappingClass() === ProductOptionMapping::class) {
  422.                 if ($column->getConfig() && $column->getConfig()['groupId']) {
  423.                     $groupIds[] = $column->getConfig()['groupId'];
  424.                 }
  425.             } else if ($column->getDataMappingClass() == ProductPropertyMapping::class) {
  426.                 if ($column->getConfig() && $column->getConfig()['propertyGroupId']) {
  427.                     $groupIds[] = $column->getConfig()['propertyGroupId'];
  428.                 }
  429.             } else if ($column->getDataMappingClass() === ProductOptionsMapping::class) {
  430.                 $ids $pagelet->getParentProduct()->getOptions()->map(function (PropertyGroupOptionEntity $entity) {
  431.                     return $entity->getGroupId();
  432.                 });
  433.                 $groupIds array_merge($groupIdsarray_unique($ids));
  434.             } else if ($column->getDataMappingClass() === ProductPropertiesMapping::class) {
  435.                 $ids $pagelet->getParentProduct()->getSortedProperties()->map(function (PropertyGroupEntity $entity) {
  436.                     return $entity->getId();
  437.                 });
  438.                 $groupIds array_merge($groupIdsarray_unique($ids));
  439.             }
  440.         }
  441.         return array_unique($groupIds);
  442.     }
  443. }