Skip to content

Commit 6b00cea

Browse files
xavierleuneXavier Leune
and
Xavier Leune
authored
feat(graphql): partial pagination for page based pagination (#6120)
Co-authored-by: Xavier Leune <[email protected]>
1 parent c01e10f commit 6b00cea

File tree

18 files changed

+416
-22
lines changed

18 files changed

+416
-22
lines changed

features/graphql/collection.feature

+121
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ Feature: GraphQL collection support
846846
itemsPerPage
847847
lastPage
848848
totalCount
849+
hasNextPage
849850
}
850851
}
851852
}
@@ -862,6 +863,7 @@ Feature: GraphQL collection support
862863
And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
863864
And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
864865
And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
866+
And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
865867
When I send the following GraphQL request:
866868
"""
867869
{
@@ -970,6 +972,7 @@ Feature: GraphQL collection support
970972
itemsPerPage
971973
lastPage
972974
totalCount
975+
hasNextPage
973976
}
974977
}
975978
}
@@ -986,3 +989,121 @@ Feature: GraphQL collection support
986989
And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
987990
And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
988991
And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
992+
And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
993+
When I send the following GraphQL request:
994+
"""
995+
{
996+
fooDummies(page: 2) {
997+
collection {
998+
id
999+
name
1000+
soManies(first: 2) {
1001+
edges {
1002+
node {
1003+
content
1004+
}
1005+
cursor
1006+
}
1007+
pageInfo {
1008+
startCursor
1009+
endCursor
1010+
hasNextPage
1011+
hasPreviousPage
1012+
}
1013+
}
1014+
}
1015+
paginationInfo {
1016+
itemsPerPage
1017+
lastPage
1018+
totalCount
1019+
hasNextPage
1020+
}
1021+
}
1022+
}
1023+
"""
1024+
Then the response status code should be 200
1025+
And the response should be in JSON
1026+
And the JSON node "data.fooDummies.collection" should have 2 elements
1027+
And the JSON node "data.fooDummies.collection[1].id" should exist
1028+
And the JSON node "data.fooDummies.collection[1].name" should exist
1029+
And the JSON node "data.fooDummies.collection[1].soManies" should exist
1030+
And the JSON node "data.fooDummies.collection[1].soManies.edges" should have 2 elements
1031+
And the JSON node "data.fooDummies.collection[1].soManies.edges[1].node.content" should be equal to "So many 1"
1032+
And the JSON node "data.fooDummies.collection[1].soManies.pageInfo.startCursor" should be equal to "MA=="
1033+
And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
1034+
And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
1035+
And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
1036+
And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be false
1037+
1038+
@createSchema
1039+
Scenario: Retrieve paginated collections using only hasNextPage
1040+
Given there are 4 fooDummy objects with fake names
1041+
When I send the following GraphQL request:
1042+
"""
1043+
{
1044+
fooDummies(page: 1, itemsPerPage: 2) {
1045+
collection {
1046+
id
1047+
name
1048+
soManies(first: 2) {
1049+
edges {
1050+
node {
1051+
content
1052+
}
1053+
cursor
1054+
}
1055+
pageInfo {
1056+
startCursor
1057+
endCursor
1058+
hasNextPage
1059+
hasPreviousPage
1060+
}
1061+
}
1062+
}
1063+
paginationInfo {
1064+
hasNextPage
1065+
}
1066+
}
1067+
}
1068+
"""
1069+
Then the response status code should be 200
1070+
And the response should be in JSON
1071+
And the JSON node "data.fooDummies.collection" should have 2 elements
1072+
And the JSON node "data.fooDummies.collection[1].id" should exist
1073+
And the JSON node "data.fooDummies.collection[1].name" should exist
1074+
And the JSON node "data.fooDummies.collection[1].soManies" should exist
1075+
And the JSON node "data.fooDummies.collection[1].soManies.edges" should have 2 elements
1076+
And the JSON node "data.fooDummies.collection[1].soManies.edges[1].node.content" should be equal to "So many 1"
1077+
And the JSON node "data.fooDummies.collection[1].soManies.pageInfo.startCursor" should be equal to "MA=="
1078+
And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
1079+
When I send the following GraphQL request:
1080+
"""
1081+
{
1082+
fooDummies(page: 2) {
1083+
collection {
1084+
id
1085+
name
1086+
soManies(first: 2) {
1087+
edges {
1088+
node {
1089+
content
1090+
}
1091+
cursor
1092+
}
1093+
pageInfo {
1094+
startCursor
1095+
endCursor
1096+
hasNextPage
1097+
hasPreviousPage
1098+
}
1099+
}
1100+
}
1101+
paginationInfo {
1102+
hasNextPage
1103+
}
1104+
}
1105+
}
1106+
"""
1107+
Then the response status code should be 200
1108+
And the response should be in JSON
1109+
And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be false

src/Doctrine/Odm/Paginator.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Doctrine\Odm;
1515

1616
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
17+
use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface;
1718
use ApiPlatform\State\Pagination\PaginatorInterface;
1819
use Doctrine\ODM\MongoDB\Iterator\Iterator;
1920
use Doctrine\ODM\MongoDB\UnitOfWork;
@@ -24,7 +25,7 @@
2425
* @author Kévin Dunglas <[email protected]>
2526
* @author Alan Poulain <[email protected]>
2627
*/
27-
final class Paginator implements \IteratorAggregate, PaginatorInterface
28+
final class Paginator implements \IteratorAggregate, PaginatorInterface, HasNextPagePaginatorInterface
2829
{
2930
public const LIMIT_ZERO_MARKER_FIELD = '___';
3031
public const LIMIT_ZERO_MARKER = 'limit0';
@@ -107,6 +108,14 @@ public function count(): int
107108
return is_countable($this->mongoDbOdmIterator->toArray()[0]['results']) ? \count($this->mongoDbOdmIterator->toArray()[0]['results']) : 0;
108109
}
109110

111+
/**
112+
* {@inheritdoc}
113+
*/
114+
public function hasNextPage(): bool
115+
{
116+
return $this->getLastPage() > $this->getCurrentPage();
117+
}
118+
110119
/**
111120
* @throws InvalidArgumentException
112121
*/

src/Doctrine/Odm/Tests/PaginatorTest.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@ class PaginatorTest extends TestCase
3131
/**
3232
* @dataProvider initializeProvider
3333
*/
34-
public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage): void
34+
public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage, bool $hasNextPage): void
3535
{
3636
$paginator = $this->getPaginator($firstResult, $maxResults, $totalItems);
3737

3838
$this->assertSame((float) $currentPage, $paginator->getCurrentPage());
3939
$this->assertSame((float) $lastPage, $paginator->getLastPage());
4040
$this->assertSame((float) $maxResults, $paginator->getItemsPerPage());
41+
$this->assertSame($hasNextPage, $paginator->hasNextPage());
4142
}
4243

4344
public function testInitializeWithFacetStageNotApplied(): void
@@ -203,8 +204,8 @@ private function getPaginatorWithNoCount($firstResult = 1, $maxResults = 15): Pa
203204
public static function initializeProvider(): array
204205
{
205206
return [
206-
'First of three pages of 15 items each' => [0, 15, 42, 1, 3],
207-
'Second of two pages of 10 items each' => [10, 10, 20, 2, 2],
207+
'First of three pages of 15 items each' => [0, 15, 42, 1, 3, true],
208+
'Second of two pages of 10 items each' => [10, 10, 20, 2, 2, false],
208209
];
209210
}
210211
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Doctrine\Orm\Extension;
15+
16+
use Doctrine\ORM\Tools\Pagination\Paginator;
17+
18+
class DoctrinePaginatorFactory
19+
{
20+
public function getPaginator($query, $fetchJoinCollection): Paginator
21+
{
22+
return new Paginator($query, $fetchJoinCollection);
23+
}
24+
}

src/Doctrine/Orm/Paginator.php

+41-1
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313

1414
namespace ApiPlatform\Doctrine\Orm;
1515

16+
use ApiPlatform\Doctrine\Orm\Extension\DoctrinePaginatorFactory;
17+
use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface;
1618
use ApiPlatform\State\Pagination\PaginatorInterface;
1719
use Doctrine\ORM\Query;
20+
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
1821

1922
/**
2023
* Decorates the Doctrine ORM paginator.
2124
*
2225
* @author Kévin Dunglas <[email protected]>
2326
*/
24-
final class Paginator extends AbstractPaginator implements PaginatorInterface, QueryAwareInterface
27+
final class Paginator extends AbstractPaginator implements PaginatorInterface, QueryAwareInterface, HasNextPagePaginatorInterface
2528
{
2629
private ?int $totalItems = null;
30+
private ?DoctrinePaginatorFactory $doctrinePaginatorFactory = null;
2731

2832
/**
2933
* {@inheritdoc}
@@ -52,4 +56,40 @@ public function getQuery(): Query
5256
{
5357
return $this->paginator->getQuery();
5458
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function hasNextPage(): bool
64+
{
65+
if (isset($this->totalItems)) {
66+
return $this->totalItems > ($this->firstResult + $this->maxResults);
67+
}
68+
69+
$cloneQuery = clone $this->paginator->getQuery();
70+
71+
$cloneQuery->setParameters(clone $this->paginator->getQuery()->getParameters());
72+
$cloneQuery->setCacheable(false);
73+
74+
foreach ($this->paginator->getQuery()->getHints() as $name => $value) {
75+
$cloneQuery->setHint($name, $value);
76+
}
77+
78+
$cloneQuery
79+
->setFirstResult($this->paginator->getQuery()->getFirstResult() + $this->paginator->getQuery()->getMaxResults())
80+
->setMaxResults(1);
81+
82+
if (null !== $this->doctrinePaginatorFactory) {
83+
$fakePaginator = $this->doctrinePaginatorFactory->getPaginator($cloneQuery, $this->paginator->getFetchJoinCollection());
84+
} else {
85+
$fakePaginator = new DoctrinePaginator($cloneQuery, $this->paginator->getFetchJoinCollection());
86+
}
87+
88+
return iterator_count($fakePaginator->getIterator()) > 0;
89+
}
90+
91+
public function setDoctrinePaginatorFactory(?DoctrinePaginatorFactory $doctrinePaginatorFactory = null): void
92+
{
93+
$this->doctrinePaginatorFactory = $doctrinePaginatorFactory;
94+
}
5595
}

src/Doctrine/Orm/Tests/Fixtures/Query.php

+41
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\Doctrine\Orm\Tests\Fixtures;
1515

16+
use Doctrine\Common\Collections\ArrayCollection;
17+
1618
/**
1719
* Replace Doctrine\ORM\Query in tests because it cannot be mocked.
1820
*/
@@ -27,4 +29,43 @@ public function getMaxResults(): ?int
2729
{
2830
return null;
2931
}
32+
33+
public function setFirstResult($firstResult): self
34+
{
35+
return $this;
36+
}
37+
38+
public function setMaxResults($maxResults): self
39+
{
40+
return $this;
41+
}
42+
43+
public function setParameters($parameters): self
44+
{
45+
return $this;
46+
}
47+
48+
public function getParameters()
49+
{
50+
return new ArrayCollection();
51+
}
52+
53+
public function setCacheable($cacheable): self
54+
{
55+
return $this;
56+
}
57+
58+
public function getHints()
59+
{
60+
return [];
61+
}
62+
63+
public function getFetchJoinCollection()
64+
{
65+
return false;
66+
}
67+
68+
public function getResult(): void
69+
{
70+
}
3071
}

0 commit comments

Comments
 (0)