Skip to content

Commit 216f71b

Browse files
committed
MC-32387: Add pagination support to categoryList query
- added new categories query to support pagination - added api-functional tests to cover new query
1 parent ef39bac commit 216f71b

File tree

8 files changed

+1905
-13
lines changed

8 files changed

+1905
-13
lines changed

app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function __construct(array $fieldToAttributeMap = [])
4545
* @param AbstractCollection $collection
4646
* @return void
4747
*/
48-
public function join(FieldNode $fieldNode, AbstractCollection $collection) : void
48+
public function join(FieldNode $fieldNode, AbstractCollection $collection): void
4949
{
5050
foreach ($this->getQueryFields($fieldNode) as $field) {
5151
$this->addFieldToCollection($collection, $field);
@@ -60,19 +60,20 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection) : voi
6060
*/
6161
public function getQueryFields(FieldNode $fieldNode): array
6262
{
63-
if (!isset($this->queryFields[$fieldNode->name->value])) {
64-
$this->queryFields[$fieldNode->name->value] = [];
63+
if (null === $this->getFieldNodeSelections($fieldNode)) {
6564
$query = $fieldNode->selectionSet->selections;
65+
$selectedFields = [];
6666
/** @var FieldNode $field */
6767
foreach ($query as $field) {
6868
if ($field->kind === 'InlineFragment') {
6969
continue;
7070
}
71-
$this->queryFields[$fieldNode->name->value][] = $field->name->value;
71+
$selectedFields[] = $field->name->value;
7272
}
73+
$this->setSelectionsForFieldNode($fieldNode, $selectedFields);
7374
}
7475

75-
return $this->queryFields[$fieldNode->name->value];
76+
return $this->getFieldNodeSelections($fieldNode);
7677
}
7778

7879
/**
@@ -83,7 +84,7 @@ public function getQueryFields(FieldNode $fieldNode): array
8384
* @param AbstractCollection $collection
8485
* @param string $field
8586
*/
86-
private function addFieldToCollection(AbstractCollection $collection, string $field)
87+
private function addFieldToCollection(AbstractCollection $collection, string $field): void
8788
{
8889
$attribute = isset($this->fieldToAttributeMap[$field]) ? $this->fieldToAttributeMap[$field] : $field;
8990

@@ -99,4 +100,28 @@ private function addFieldToCollection(AbstractCollection $collection, string $fi
99100
}
100101
}
101102
}
103+
104+
/**
105+
* Get the fields selections for a query node
106+
*
107+
* @param FieldNode $fieldNode
108+
* @return array|null
109+
*/
110+
private function getFieldNodeSelections(FieldNode $fieldNode): ?array
111+
{
112+
return $this->queryFields[$fieldNode->name->value][$fieldNode->name->loc->start] ?? null;
113+
}
114+
115+
/**
116+
* Set the field selections for a query node
117+
*
118+
* Index nodes by name and position so nodes with same name don't collide
119+
*
120+
* @param FieldNode $fieldNode
121+
* @param array $selectedFields
122+
*/
123+
private function setSelectionsForFieldNode(FieldNode $fieldNode, array $selectedFields): void
124+
{
125+
$this->queryFields[$fieldNode->name->value][$fieldNode->name->loc->start] = $selectedFields;
126+
}
102127
}

app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Magento\Catalog\Api\Data\CategoryInterface;
1212
use Magento\Framework\App\Config\ScopeConfigInterface;
1313
use Magento\Framework\Exception\InputException;
14+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1415
use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter;
1516
use Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match;
1617
use Magento\Search\Model\Query;
@@ -19,7 +20,7 @@
1920
use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder;
2021

2122
/**
22-
* Category filter allows to filter collection using 'id, url_key, name' from search criteria.
23+
* Category filter allows filtering category results by attributes.
2324
*/
2425
class CategoryFilter
2526
{
@@ -54,25 +55,50 @@ public function __construct(
5455
}
5556

5657
/**
57-
* Search for categories, return list of ids
58+
* Search for categories
5859
*
5960
* @param array $criteria
6061
* @param StoreInterface $store
6162
* @return int[]
6263
* @throws InputException
6364
*/
64-
public function getResult(array $criteria, StoreInterface $store): array
65+
public function getResult(array $criteria, StoreInterface $store)
6566
{
6667
$categoryIds = [];
67-
6868
$criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store);
6969
$criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1];
7070
$searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria);
71+
$pageSize = $criteria['pageSize'] ?? 20;
72+
$currentPage = $criteria['currentPage'] ?? 1;
73+
$searchCriteria->setPageSize($pageSize)->setCurrentPage($currentPage);
74+
7175
$categories = $this->categoryList->getList($searchCriteria);
7276
foreach ($categories->getItems() as $category) {
7377
$categoryIds[] = (int)$category->getId();
7478
}
75-
return $categoryIds;
79+
80+
$totalPages = 0;
81+
if ($categories->getTotalCount() > 0 && $searchCriteria->getPageSize() > 0) {
82+
$totalPages = ceil($categories->getTotalCount() / $searchCriteria->getPageSize());
83+
}
84+
if ($searchCriteria->getCurrentPage() > $totalPages && $categories->getTotalCount() > 0) {
85+
throw new GraphQlInputException(
86+
__(
87+
'currentPage value %1 specified is greater than the %2 page(s) available.',
88+
[$searchCriteria->getCurrentPage(), $totalPages]
89+
)
90+
);
91+
}
92+
93+
return [
94+
'category_ids' => $categoryIds,
95+
'total_count' => $categories->getTotalCount(),
96+
'page_info' => [
97+
'total_pages' => $totalPages,
98+
'page_size' => $searchCriteria->getPageSize(),
99+
'current_page' => $searchCriteria->getCurrentPage(),
100+
]
101+
];
76102
}
77103

78104
/**
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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;
9+
10+
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree;
11+
use Magento\Framework\GraphQl\Config\Element\Field;
12+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
13+
use Magento\Framework\GraphQl\Query\ResolverInterface;
14+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
15+
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree;
16+
use Magento\CatalogGraphQl\Model\Category\CategoryFilter;
17+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
18+
19+
/**
20+
* Categories resolver, used for GraphQL category data request processing.
21+
*/
22+
class CategoriesQuery implements ResolverInterface
23+
{
24+
/**
25+
* @var CategoryTree
26+
*/
27+
private $categoryTree;
28+
29+
/**
30+
* @var CollectionFactory
31+
*/
32+
private $collectionFactory;
33+
34+
/**
35+
* @var CategoryFilter
36+
*/
37+
private $categoryFilter;
38+
39+
/**
40+
* @var ExtractDataFromCategoryTree
41+
*/
42+
private $extractDataFromCategoryTree;
43+
44+
/**
45+
* @param CategoryTree $categoryTree
46+
* @param ExtractDataFromCategoryTree $extractDataFromCategoryTree
47+
* @param CategoryFilter $categoryFilter
48+
* @param CollectionFactory $collectionFactory
49+
*/
50+
public function __construct(
51+
CategoryTree $categoryTree,
52+
ExtractDataFromCategoryTree $extractDataFromCategoryTree,
53+
CategoryFilter $categoryFilter,
54+
CollectionFactory $collectionFactory
55+
) {
56+
$this->categoryTree = $categoryTree;
57+
$this->extractDataFromCategoryTree = $extractDataFromCategoryTree;
58+
$this->categoryFilter = $categoryFilter;
59+
$this->collectionFactory = $collectionFactory;
60+
}
61+
62+
/**
63+
* @inheritdoc
64+
*/
65+
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
66+
{
67+
$store = $context->getExtensionAttributes()->getStore();
68+
69+
if (isset($args['currentPage']) && $args['currentPage'] < 1) {
70+
throw new GraphQlInputException(__('currentPage value must be greater than 0.'));
71+
}
72+
if (isset($args['pageSize']) && $args['pageSize'] < 1) {
73+
throw new GraphQlInputException(__('pageSize value must be greater than 0.'));
74+
}
75+
if (!isset($args['filters'])) {
76+
//When no filters are specified, get the root category
77+
$args['filters']['ids'] = ['eq' => $store->getRootCategoryId()];
78+
}
79+
80+
$filterResult = $this->categoryFilter->getResult($args, $store);
81+
82+
$rootCategoryIds = $filterResult['category_ids'];
83+
$filterResult['items'] = $this->fetchCategories($rootCategoryIds, $info);
84+
return $filterResult;
85+
}
86+
87+
/**
88+
* Fetch category tree data
89+
*
90+
* @param array $categoryIds
91+
* @param ResolveInfo $info
92+
* @return array
93+
*/
94+
private function fetchCategories(array $categoryIds, ResolveInfo $info)
95+
{
96+
$fetchedCategories = [];
97+
foreach ($categoryIds as $categoryId) {
98+
$categoryTree = $this->categoryTree->getTree($info, $categoryId);
99+
if (empty($categoryTree)) {
100+
continue;
101+
}
102+
$fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree));
103+
}
104+
105+
return $fetchedCategories;
106+
}
107+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
6565
$args['filters']['ids'] = ['eq' => $store->getRootCategoryId()];
6666
}
6767
try {
68-
$rootCategoryIds = $this->categoryFilter->getResult($args, $store);
68+
$filterResults = $this->categoryFilter->getResult($args, $store);
69+
$rootCategoryIds = $filterResults['category_ids'];
6970
} catch (InputException $e) {
7071
throw new GraphQlInputException(__($e->getMessage()));
7172
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ type Query {
1515
): CategoryTree
1616
@resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity")
1717
categoryList(
18-
filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.")
18+
filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.")
1919
): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity")
20+
categories (
21+
filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.")
22+
pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional.")
23+
currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1.")
24+
): CategoryResult @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoriesQuery")
2025
}
2126

2227
type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") {
@@ -133,6 +138,12 @@ type CategoryTree implements CategoryInterface @doc(description: "Category Tree
133138
children: [CategoryTree] @doc(description: "Child categories tree.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree")
134139
}
135140

141+
type CategoryResult @doc(description: "Collection of CategoryTree object and pagination information.") {
142+
items: [CategoryTree] @doc(description: "List of categories that match filter criteria.")
143+
page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.")
144+
total_count: Int @doc(description: "The total number of categories that match the criteria.")
145+
}
146+
136147
type CustomizableDateOption implements CustomizableOptionInterface @doc(description: "CustomizableDateOption contains information about a date picker that is defined as part of a customizable option.") {
137148
value: CustomizableDateValue @doc(description: "An object that defines a date field in a customizable option.")
138149
product_sku: String @doc(description: "The Stock Keeping Unit of the base product.")

0 commit comments

Comments
 (0)