Skip to content

Commit 340a8f4

Browse files
committed
Merge branch 'MC-42795-PR' into MC-42795
2 parents c9bd7fd + 9dae57e commit 340a8f4

File tree

4 files changed

+330
-1
lines changed

4 files changed

+330
-1
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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\Model\Resolver\Products\Query;
9+
10+
use Magento\Catalog\Model\CategoryRepository;
11+
use Magento\Framework\Api\Search\AggregationValueInterface;
12+
use Magento\Framework\Exception\NoSuchEntityException;
13+
use Magento\Framework\Search\Response\Aggregation;
14+
use Magento\Framework\Search\Response\AggregationFactory;
15+
use Magento\Framework\Search\Response\Bucket;
16+
use Magento\Framework\Search\Response\BucketFactory;
17+
use Magento\Store\Model\StoreManagerInterface;
18+
19+
/**
20+
* Resolve category aggregation.
21+
*/
22+
class ResolveCategoryAggregation
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const CATEGORY_BUCKET = 'category_bucket';
28+
29+
/**
30+
* @var string
31+
*/
32+
private const BUCKETS = 'buckets';
33+
34+
/**
35+
* @var AggregationFactory
36+
*/
37+
private $aggregationFactory;
38+
39+
/**
40+
* @var BucketFactory
41+
*/
42+
private $bucketFactory;
43+
44+
/**
45+
* @var StoreManagerInterface
46+
*/
47+
private $storeManager;
48+
49+
/**
50+
* @var CategoryRepository
51+
*/
52+
private $categoryRepository;
53+
54+
/**
55+
* @var array
56+
*/
57+
private $resolvedChildrenIds = [];
58+
59+
/**
60+
* @param AggregationFactory $aggregationFactory
61+
* @param BucketFactory $bucketFactory
62+
* @param StoreManagerInterface $storeManager
63+
* @param CategoryRepository $categoryRepository
64+
*/
65+
public function __construct(
66+
AggregationFactory $aggregationFactory,
67+
BucketFactory $bucketFactory,
68+
StoreManagerInterface $storeManager,
69+
CategoryRepository $categoryRepository
70+
) {
71+
$this->aggregationFactory = $aggregationFactory;
72+
$this->bucketFactory = $bucketFactory;
73+
$this->storeManager = $storeManager;
74+
$this->categoryRepository = $categoryRepository;
75+
}
76+
77+
/**
78+
* Get resolved category aggregation.
79+
*
80+
* @param array $categoryFilter
81+
* @param Bucket[] $bucketList
82+
* @return Aggregation
83+
*/
84+
public function getResolvedCategoryAggregation(array $categoryFilter, array $bucketList): Aggregation
85+
{
86+
$categoryBucket = $bucketList[self::CATEGORY_BUCKET] ?? [];
87+
$values = $categoryBucket->getValues();
88+
$condition = array_key_first($categoryFilter);
89+
$searchableCategoryValue = $categoryFilter[$condition] ?? 0;
90+
91+
if (!$searchableCategoryValue || empty($values)) {
92+
return $this->aggregationFactory->create([self::BUCKETS => $bucketList]);
93+
}
94+
$resolvedBucketList = $bucketList;
95+
96+
try {
97+
if (!is_array($searchableCategoryValue)) {
98+
$resolvedValues = $this->getValidCategories((int) $searchableCategoryValue, $values);
99+
} else {
100+
$categoryResolvedValues = [];
101+
102+
foreach ($searchableCategoryValue as $categoryId) {
103+
$categoryResolvedValues[] = $this->getValidCategories((int) $categoryId, $values);
104+
}
105+
106+
$resolvedValues = call_user_func_array('array_merge', $categoryResolvedValues);;
107+
}
108+
} catch (NoSuchEntityException $e) {
109+
return $this->aggregationFactory->create([self::BUCKETS => $bucketList]);
110+
}
111+
$resolvedCategoryBucket = $this->bucketFactory->create(
112+
[
113+
'name' => self::CATEGORY_BUCKET,
114+
'values' => $resolvedValues
115+
]
116+
);
117+
$resolvedBucketList[self::CATEGORY_BUCKET] = $resolvedCategoryBucket;
118+
119+
return $this->aggregationFactory->create([self::BUCKETS => $resolvedBucketList]);
120+
}
121+
122+
/**
123+
* Check is valid searchable category children, and return all aggregations with updated categories list.
124+
*
125+
* @param int $searchableCategoryId
126+
* @param AggregationValueInterface[] $aggregationValues
127+
* @return AggregationValueInterface[]
128+
* @throws NoSuchEntityException
129+
*/
130+
private function getValidCategories(int $searchableCategoryId, array $aggregationValues): array
131+
{
132+
$storeId = (int) $this->storeManager->getStore()->getId();
133+
$searchableCategory = $this->categoryRepository->get($searchableCategoryId, $storeId);
134+
$childrenList = $searchableCategory->getChildrenCategories();
135+
$resolvedList = [];
136+
$validChildIdList = [];
137+
138+
foreach ($childrenList as $child) {
139+
if (!$child->getIsActive()) {
140+
continue;
141+
}
142+
143+
$validChildIdList[] = $child->getId();
144+
}
145+
146+
foreach ($aggregationValues as $bucketValue) {
147+
$childCategoryId = (int) $bucketValue->getValue();
148+
149+
if (!in_array($childCategoryId, $validChildIdList)
150+
|| in_array($childCategoryId, $this->resolvedChildrenIds)
151+
) {
152+
continue;
153+
}
154+
155+
$resolvedList[] = $bucketValue;
156+
$this->resolvedChildrenIds[] = $childCategoryId;
157+
}
158+
159+
return $resolvedList;
160+
}
161+
}

app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class Search implements ProductQueryInterface
5555
*/
5656
private $productsProvider;
5757

58+
/**
59+
* @var ResolveCategoryAggregation
60+
*/
61+
private $resolveCategoryAggregation;
62+
5863
/**
5964
* @var SearchCriteriaBuilder
6065
*/
@@ -68,6 +73,7 @@ class Search implements ProductQueryInterface
6873
* @param ProductSearch $productsProvider
6974
* @param SearchCriteriaBuilder $searchCriteriaBuilder
7075
* @param ArgumentsProcessorInterface|null $argsSelection
76+
* @param ResolveCategoryAggregation $resolveCategoryAggregation
7177
*/
7278
public function __construct(
7379
SearchInterface $search,
@@ -76,6 +82,7 @@ public function __construct(
7682
FieldSelection $fieldSelection,
7783
ProductSearch $productsProvider,
7884
SearchCriteriaBuilder $searchCriteriaBuilder,
85+
ResolveCategoryAggregation $resolveCategoryAggregation,
7986
ArgumentsProcessorInterface $argsSelection = null
8087
) {
8188
$this->search = $search;
@@ -84,6 +91,8 @@ public function __construct(
8491
$this->fieldSelection = $fieldSelection;
8592
$this->productsProvider = $productsProvider;
8693
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
94+
$this->resolveCategoryAggregation = $resolveCategoryAggregation ?: ObjectManager::getInstance()
95+
->get(ResolveCategoryAggregation::class);
8796
$this->argsSelection = $argsSelection ?: ObjectManager::getInstance()
8897
->get(ArgumentsProcessorInterface::class);
8998
}
@@ -124,6 +133,17 @@ public function getResult(
124133

125134
$totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0;
126135

136+
$aggregations = $itemsResults->getAggregations();
137+
$bucketList = $aggregations->getBuckets();
138+
$categoryFilter = $args['filter']['category_id'] ?? [];
139+
140+
if (!empty($categoryFilter) && isset($bucketList[ResolveCategoryAggregation::CATEGORY_BUCKET])) {
141+
$aggregations = $this->resolveCategoryAggregation->getResolvedCategoryAggregation(
142+
$categoryFilter,
143+
$bucketList
144+
);
145+
}
146+
127147
$productArray = [];
128148
/** @var \Magento\Catalog\Model\Product $product */
129149
foreach ($searchResults->getItems() as $product) {
@@ -135,7 +155,7 @@ public function getResult(
135155
[
136156
'totalCount' => $searchResults->getTotalCount(),
137157
'productsSearchResult' => $productArray,
138-
'searchAggregation' => $itemsResults->getAggregations(),
158+
'searchAggregation' => $aggregations,
139159
'pageSize' => $realPageSize,
140160
'currentPage' => $realCurrentPage,
141161
'totalPages' => $totalPages,
@@ -149,6 +169,7 @@ public function getResult(
149169
* @param array $args
150170
* @param ResolveInfo $info
151171
* @return SearchCriteriaInterface
172+
* @throws GraphQlInputException
152173
*/
153174
private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCriteriaInterface
154175
{

app/code/Magento/CatalogGraphQl/etc/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
</arguments>
8686
</type>
8787

88+
<type name="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search">
89+
<arguments>
90+
<argument name="resolveCategoryAggregation" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\ResolveCategoryAggregation\Proxy</argument>
91+
</arguments>
92+
</type>
93+
8894
<type name="Magento\Framework\Search\Request\Config\FilesystemReader">
8995
<plugin name="productAttributesDynamicFields" type="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader" />
9096
</type>
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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\GraphQl\Catalog;
9+
10+
use Exception;
11+
use Magento\TestFramework\TestCase\GraphQlAbstract;
12+
13+
/**
14+
* Test of search by category ID aggregation.
15+
*/
16+
class ProductSearchCategoryAggregationsTest extends GraphQlAbstract
17+
{
18+
/**
19+
* Test category_id aggregation on filter by "eq" category ID condition.
20+
*
21+
* @throws Exception
22+
*
23+
* @magentoApiDataFixture Magento/Catalog/_files/categories.php
24+
*/
25+
public function testAggregationEqCategory()
26+
{
27+
$filterValue = '{category_id: {eq: "2"}}';
28+
$categoryAggregation = $this->aggregationCategoryTesting($filterValue);
29+
30+
$this->assertEquals(4, $categoryAggregation['count']);
31+
32+
$expectedOptions = $this->getCategoryTwoOptions();
33+
34+
$this->assertEquals($expectedOptions, $categoryAggregation['options']);
35+
}
36+
37+
/**
38+
* Test category_id aggregation on filter by "in" category ID condition.
39+
*
40+
* @throws Exception
41+
*
42+
* @magentoApiDataFixture Magento/Catalog/_files/categories.php
43+
*/
44+
public function testAggregationInCategory()
45+
{
46+
$filterValue = '{category_id: {in: ["3","2"]}}';
47+
$categoryAggregation = $this->aggregationCategoryTesting($filterValue);
48+
49+
$this->assertEquals(6, $categoryAggregation['count']);
50+
51+
$expectedOptions = array_merge($this->getCategoryThreeOptions(), $this->getCategoryTwoOptions());
52+
53+
$this->assertEquals($expectedOptions, $categoryAggregation['options']);
54+
}
55+
56+
/**
57+
* @param string $filterValue
58+
*
59+
* @return array
60+
*
61+
* @throws Exception
62+
*/
63+
private function aggregationCategoryTesting(string $filterValue): array
64+
{
65+
$query = $this->getGraphQlQuery($filterValue);
66+
$result = $this->graphQlQuery($query);
67+
68+
$this->assertArrayNotHasKey('errors', $result);
69+
$this->assertArrayHasKey('aggregations', $result['products']);
70+
71+
$categoryAggregation = array_filter(
72+
$result['products']['aggregations'],
73+
function ($a) {
74+
return $a['attribute_code'] == 'category_id';
75+
}
76+
);
77+
78+
$this->assertNotEmpty($categoryAggregation);
79+
$categoryAggregation = reset($categoryAggregation);
80+
$this->assertEquals('Category', $categoryAggregation['label']);
81+
82+
return $categoryAggregation;
83+
}
84+
85+
/**
86+
* Category ID 2, category_id aggregation options.
87+
*
88+
* @return array<string,string>
89+
*/
90+
private function getCategoryTwoOptions(): array
91+
{
92+
return [
93+
['label' => 'Category 1', 'value'=> '3', 'count' => '3'],
94+
['label' => 'Movable Position 2', 'value'=> '10', 'count' => '1'],
95+
['label' => 'Movable Position 3', 'value'=> '11', 'count' => '1'],
96+
['label' => 'Category 12', 'value'=> '12', 'count' => '1']
97+
];
98+
}
99+
100+
/**
101+
* Category ID 3, category_id aggregation options.
102+
*
103+
* @return array<string,string>
104+
*/
105+
private function getCategoryThreeOptions(): array
106+
{
107+
return [
108+
['label' => 'Category 1.1', 'value'=> '4', 'count' => '2'],
109+
['label' => 'Category 1.2', 'value'=> '13', 'count' => '2']
110+
];
111+
}
112+
113+
/**
114+
* Get graphQl query.
115+
*
116+
* @param string $categoryList
117+
*
118+
* @return string
119+
*/
120+
private function getGraphQlQuery(string $categoryList): string
121+
{
122+
return <<<QUERY
123+
{
124+
products(filter: {$categoryList}) {
125+
total_count
126+
items { sku }
127+
aggregations {
128+
attribute_code
129+
count
130+
label
131+
options {
132+
count
133+
label
134+
value
135+
}
136+
}
137+
}
138+
}
139+
QUERY;
140+
}
141+
}

0 commit comments

Comments
 (0)