Skip to content

Commit c9bd7fd

Browse files
committed
MC-42795: GraphQl products query layered navigation filters return incorrect child categories list
1 parent d1ba944 commit c9bd7fd

File tree

4 files changed

+192
-3
lines changed

4 files changed

+192
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Aggregations;
9+
10+
use Magento\Catalog\Api\CategoryListInterface;
11+
use Magento\Framework\Api\SearchCriteriaBuilder;
12+
use Magento\Framework\Search\Response\Aggregation;
13+
use Magento\Framework\Search\Response\AggregationFactory;
14+
use Magento\Framework\Search\Response\BucketFactory;
15+
use Magento\Store\Model\StoreManagerInterface;
16+
use Magento\Framework\Api\Search\AggregationInterface;
17+
18+
/**
19+
* Include only subcategories of category in aggregation
20+
*/
21+
class IncludeSubcategoriesOnly
22+
{
23+
/**
24+
* @var string
25+
*/
26+
private $categoryBucket = 'category_bucket';
27+
28+
/**
29+
* @var string
30+
*/
31+
private $bucketsName = 'buckets';
32+
33+
/**
34+
* @var AggregationFactory
35+
*/
36+
private $aggregationFactory;
37+
38+
/**
39+
* @var BucketFactory
40+
*/
41+
private $bucketFactory;
42+
43+
/**
44+
* @var StoreManagerInterface
45+
*/
46+
private $storeManager;
47+
48+
/**
49+
* @var CategoryListInterface
50+
*/
51+
private $categoryList;
52+
53+
/**
54+
* @var array
55+
*/
56+
private $filter = [];
57+
58+
/**
59+
* @var SearchCriteriaBuilder
60+
*/
61+
private $searchCriteriaBuilder;
62+
63+
/**
64+
* @param AggregationFactory $aggregationFactory
65+
* @param BucketFactory $bucketFactory
66+
* @param StoreManagerInterface $storeManager
67+
* @param CategoryListInterface $categoryList
68+
* @param SearchCriteriaBuilder $searchCriteriaBuilder
69+
*/
70+
public function __construct(
71+
AggregationFactory $aggregationFactory,
72+
BucketFactory $bucketFactory,
73+
StoreManagerInterface $storeManager,
74+
CategoryListInterface $categoryList,
75+
SearchCriteriaBuilder $searchCriteriaBuilder
76+
) {
77+
$this->aggregationFactory = $aggregationFactory;
78+
$this->bucketFactory = $bucketFactory;
79+
$this->storeManager = $storeManager;
80+
$this->categoryList = $categoryList;
81+
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
82+
}
83+
84+
/**
85+
* Filter category aggregation to include only subcategories of requested category
86+
*
87+
* @param AggregationInterface $aggregation
88+
* @param int|null $storeId
89+
* @return Aggregation
90+
*/
91+
public function filter(AggregationInterface $aggregation, ?int $storeId): Aggregation
92+
{
93+
$categoryIdsRequested = $this->filter['category'] ?? null;
94+
if ($categoryIdsRequested === null) {
95+
return $aggregation;
96+
}
97+
$buckets = $aggregation->getBuckets();
98+
$categoryBucket = $buckets[$this->categoryBucket] ?? null;
99+
if ($categoryBucket === null || empty($categoryBucket->getValues())) {
100+
return $aggregation;
101+
}
102+
$categoryIdsRequested = is_array($categoryIdsRequested) ? $categoryIdsRequested : [$categoryIdsRequested];
103+
$bucketValuesFiltered = $this->filterBucketValues(
104+
$categoryBucket->getValues(),
105+
$categoryIdsRequested,
106+
$storeId
107+
);
108+
$categoryBucketResolved = $this->bucketFactory->create(
109+
[
110+
'name' => $this->categoryBucket,
111+
'values' => $bucketValuesFiltered
112+
]
113+
);
114+
$buckets[$this->categoryBucket] = $categoryBucketResolved;
115+
return $this->aggregationFactory->create([$this->bucketsName => $buckets]);
116+
}
117+
118+
/**
119+
* Set filter for categories aggregation
120+
*
121+
* @param array $filter
122+
*/
123+
public function setFilter(array $filter): void
124+
{
125+
$this->filter = $filter;
126+
}
127+
128+
/**
129+
* Filter bucket values to include only subcategories of requested category
130+
*
131+
* @param array $categoryBucketValues
132+
* @param array $categoryIdsRequested
133+
* @param int|null $storeId
134+
* @return array
135+
*/
136+
private function filterBucketValues(
137+
array $categoryBucketValues,
138+
array $categoryIdsRequested,
139+
?int $storeId
140+
): array {
141+
$categoryChildIds = [];
142+
$storeId = $storeId !== null ? $storeId : $this->storeManager->getStore()->getId();
143+
$searchCriteria = $this->searchCriteriaBuilder
144+
->addFilter('entity_id', $categoryIdsRequested, 'in')
145+
->create();
146+
$categoriesRequested = $this->categoryList->getList($searchCriteria);
147+
foreach ($categoriesRequested->getItems() as $category) {
148+
$category->setStoreId($storeId);
149+
$childrenIds = $category->getChildren();
150+
if ($childrenIds) {
151+
$categoryChildIds = array_merge($categoryChildIds, explode(',', $childrenIds));
152+
}
153+
}
154+
foreach ($categoryBucketValues as $key => $bucketValue) {
155+
$categoryId = (int)$bucketValue->getValue();
156+
if (!in_array($categoryId, $categoryChildIds)) {
157+
unset($categoryBucketValues[$key]);
158+
}
159+
}
160+
return array_values($categoryBucketValues);
161+
}
162+
}

app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Magento\Framework\Api\Search\BucketInterface;
1717
use Magento\Framework\App\ResourceConnection;
1818
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter;
19+
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Aggregations\IncludeSubcategoriesOnly;
1920

2021
/**
2122
* @inheritdoc
@@ -62,6 +63,11 @@ class Category implements LayerBuilderInterface
6263
*/
6364
private $layerFormatter;
6465

66+
/**
67+
* @var IncludeSubcategoriesOnly
68+
*/
69+
private $includeSubcategoriesOnly;
70+
6571
/**
6672
* @param CategoryAttributeQuery $categoryAttributeQuery
6773
* @param CategoryAttributesMapper $attributesMapper
@@ -74,13 +80,15 @@ public function __construct(
7480
CategoryAttributesMapper $attributesMapper,
7581
RootCategoryProvider $rootCategoryProvider,
7682
ResourceConnection $resourceConnection,
77-
LayerFormatter $layerFormatter
83+
LayerFormatter $layerFormatter,
84+
IncludeSubcategoriesOnly $includeSubcategoriesOnly
7885
) {
7986
$this->categoryAttributeQuery = $categoryAttributeQuery;
8087
$this->attributesMapper = $attributesMapper;
8188
$this->resourceConnection = $resourceConnection;
8289
$this->rootCategoryProvider = $rootCategoryProvider;
8390
$this->layerFormatter = $layerFormatter;
91+
$this->includeSubcategoriesOnly = $includeSubcategoriesOnly;
8492
}
8593

8694
/**
@@ -90,6 +98,7 @@ public function __construct(
9098
*/
9199
public function build(AggregationInterface $aggregation, ?int $storeId): array
92100
{
101+
$aggregation = $this->includeSubcategoriesOnly->filter($aggregation, $storeId);
93102
$bucket = $aggregation->getBucket(self::CATEGORY_BUCKET);
94103
if ($this->isBucketEmpty($bucket)) {
95104
return [];

app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace Magento\CatalogGraphQl\Model\Resolver;
99

1010
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder;
11+
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Aggregations\IncludeSubcategoriesOnly;
1112
use Magento\Directory\Model\PriceCurrency;
1213
use Magento\Framework\App\ObjectManager;
1314
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -35,6 +36,11 @@ class Aggregations implements ResolverInterface
3536
*/
3637
private $priceCurrency;
3738

39+
/**
40+
* @var IncludeSubcategoriesOnly
41+
*/
42+
private $includeSubcategoriesOnly;
43+
3844
/**
3945
* @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider
4046
* @param LayerBuilder $layerBuilder
@@ -43,11 +49,14 @@ class Aggregations implements ResolverInterface
4349
public function __construct(
4450
\Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider,
4551
LayerBuilder $layerBuilder,
46-
PriceCurrency $priceCurrency = null
52+
PriceCurrency $priceCurrency = null,
53+
IncludeSubcategoriesOnly $includeSubcategoriesOnly = null
4754
) {
4855
$this->filtersDataProvider = $filtersDataProvider;
4956
$this->layerBuilder = $layerBuilder;
5057
$this->priceCurrency = $priceCurrency ?: ObjectManager::getInstance()->get(PriceCurrency::class);
58+
$this->includeSubcategoriesOnly = $includeSubcategoriesOnly
59+
?: ObjectManager::getInstance()->get(IncludeSubcategoriesOnly::class);
5160
}
5261

5362
/**
@@ -67,6 +76,11 @@ public function resolve(
6776
$aggregations = $value['search_result']->getSearchAggregation();
6877

6978
if ($aggregations) {
79+
$categoryFilter = $value['categories'] ?? [];
80+
$includeSubcategoriesOnly = $args['filter']['includeSubcategoriesOnly'] ?? false;
81+
if ($includeSubcategoriesOnly && !empty($categoryFilter)) {
82+
$this->includeSubcategoriesOnly->setFilter(['category' => $categoryFilter]);
83+
}
7084
/** @var StoreInterface $store */
7185
$store = $context->getExtensionAttributes()->getStore();
7286
$storeId = (int)$store->getId();

app/code/Magento/CatalogGraphQl/etc/schema.graphqls

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,14 @@ type Products @doc(description: "The Products object is the top-level object ret
320320
page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.")
321321
total_count: Int @doc(description: "The number of products that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.")
322322
filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead")
323-
aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations")
323+
aggregations (filter: AggregationsFilterInput): [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations")
324324
sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields")
325325
}
326326

327+
input AggregationsFilterInput {
328+
includeSubcategoriesOnly: Boolean = false @doc(description: "Flag to include only subcategories of requested category.")
329+
}
330+
327331
type CategoryProducts @doc(description: "The category products object returned in the Category query.") {
328332
items: [ProductInterface] @doc(description: "An array of products that are assigned to the category.")
329333
page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.")

0 commit comments

Comments
 (0)