Skip to content

Commit cc4f3c9

Browse files
meyerbaptistedunglas
authored andcommitted
Refactoring of the pagination to get ride of HttpFoundation (#2446)
* Refactoring of the pagination to get ride of HttpFoundation * Fix unit tests
1 parent c20b86c commit cc4f3c9

File tree

11 files changed

+752
-178
lines changed

11 files changed

+752
-178
lines changed

.travis.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ jobs:
99
include:
1010
- php: '7.1'
1111
- php: '7.2'
12-
- php: '7.2'
13-
services:
14-
- elasticsearch
15-
env: APP_ENV=elasticsearch
1612
- php: '7.3'
1713
- php: '7.3'
1814
env: deps=low
@@ -30,6 +26,10 @@ jobs:
3026
before_script:
3127
- mysql -e 'CREATE DATABASE api_platform_test;'
3228
env: APP_ENV=mysql
29+
- php: '7.3'
30+
services:
31+
- elasticsearch
32+
env: APP_ENV=elasticsearch
3333
allow_failures:
3434
- env: SYMFONY_DEPRECATIONS_HELPER=0
3535
fast_finish: true

src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php

Lines changed: 164 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
use ApiPlatform\Core\DataProvider\Pagination;
2121
use ApiPlatform\Core\Exception\InvalidArgumentException;
2222
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2324
use Doctrine\Common\Persistence\ManagerRegistry;
2425
use Doctrine\ORM\QueryBuilder;
2526
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
27+
use Symfony\Component\HttpFoundation\Request;
2628
use Symfony\Component\HttpFoundation\RequestStack;
2729

2830
/**
@@ -34,7 +36,19 @@
3436
final class PaginationExtension implements ContextAwareQueryResultCollectionExtensionInterface
3537
{
3638
private $managerRegistry;
39+
private $requestStack;
3740
private $resourceMetadataFactory;
41+
private $enabled;
42+
private $clientEnabled;
43+
private $clientItemsPerPage;
44+
private $itemsPerPage;
45+
private $pageParameterName;
46+
private $enabledParameterName;
47+
private $itemsPerPageParameterName;
48+
private $maximumItemPerPage;
49+
private $partial;
50+
private $clientPartial;
51+
private $partialParameterName;
3852
private $pagination;
3953

4054
/**
@@ -47,45 +61,40 @@ public function __construct(ManagerRegistry $managerRegistry, /* ResourceMetadat
4761
@trigger_error(sprintf('Passing an instance of "%s" as second argument of "%s" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.', RequestStack::class, self::class, ResourceMetadataFactoryInterface::class), E_USER_DEPRECATED);
4862
@trigger_error(sprintf('Passing an instance of "%s" as third argument of "%s" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.', ResourceMetadataFactoryInterface::class, self::class, Pagination::class), E_USER_DEPRECATED);
4963

50-
$requestStack = $resourceMetadataFactory;
64+
$this->requestStack = $resourceMetadataFactory;
5165
$resourceMetadataFactory = $pagination;
66+
$pagination = null;
5267

68+
$args = \array_slice(\func_get_args(), 3);
5369
$legacyPaginationArgs = [
54-
3 => ['arg_name' => 'enabled', 'option_name' => 'enabled', 'type' => 'bool', 'default' => true],
55-
4 => ['arg_name' => 'clientEnabled', 'option_name' => 'client_enabled', 'type' => 'bool', 'default' => false],
56-
5 => ['arg_name' => 'clientItemsPerPage', 'option_name' => 'client_items_per_page', 'type' => 'bool', 'default' => false],
57-
6 => ['arg_name' => 'itemsPerPage', 'option_name' => 'items_per_page', 'type' => 'int', 'default' => 30],
58-
7 => ['arg_name' => 'pageParameterName', 'option_name' => 'page_parameter_name', 'type' => 'string', 'default' => 'page'],
59-
8 => ['arg_name' => 'enabledParameterName', 'option_name' => 'enabled_parameter_name', 'type' => 'string', 'default' => 'pagination'],
60-
9 => ['arg_name' => 'itemsPerPageParameterName', 'option_name' => 'items_per_page_parameter_name', 'type' => 'string', 'default' => 'itemsPerPage'],
61-
10 => ['arg_name' => 'maximumItemPerPage', 'option_name' => 'maximum_items_per_page', 'type' => 'int', 'default' => null],
62-
11 => ['arg_name' => 'partial', 'option_name' => 'partial', 'type' => 'bool', 'default' => false],
63-
12 => ['arg_name' => 'clientPartial', 'option_name' => 'client_partial', 'type' => 'bool', 'default' => false],
64-
13 => ['arg_name' => 'partialParameterName', 'option_name' => 'partial_parameter_name', 'type' => 'string', 'default' => 'partial'],
70+
['arg_name' => 'enabled', 'type' => 'bool', 'default' => true],
71+
['arg_name' => 'clientEnabled', 'type' => 'bool', 'default' => false],
72+
['arg_name' => 'clientItemsPerPage', 'type' => 'bool', 'default' => false],
73+
['arg_name' => 'itemsPerPage', 'type' => 'int', 'default' => 30],
74+
['arg_name' => 'pageParameterName', 'type' => 'string', 'default' => 'page'],
75+
['arg_name' => 'enabledParameterName', 'type' => 'string', 'default' => 'pagination'],
76+
['arg_name' => 'itemsPerPageParameterName', 'type' => 'string', 'default' => 'itemsPerPage'],
77+
['arg_name' => 'maximumItemPerPage', 'type' => 'int', 'default' => null],
78+
['arg_name' => 'partial', 'type' => 'bool', 'default' => false],
79+
['arg_name' => 'clientPartial', 'type' => 'bool', 'default' => false],
80+
['arg_name' => 'partialParameterName', 'type' => 'string', 'default' => 'partial'],
6581
];
6682

67-
$paginationOptions = array_column($legacyPaginationArgs, 'default', 'option_name');
83+
foreach ($legacyPaginationArgs as $pos => $arg) {
84+
if (array_key_exists($pos, $args)) {
85+
@trigger_error(sprintf('Passing "$%s" arguments is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" as third argument instead.', implode('", "$', array_column($legacyPaginationArgs, 'arg_name')), Paginator::class), E_USER_DEPRECATED);
6886

69-
if (0 < \count($legacyArgs = \array_slice(\func_get_args(), 3, null, true))) {
70-
@trigger_error(sprintf('Passing "$%s" arguments is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" as third argument instead.', implode('", "$', array_column($legacyPaginationArgs, 'arg_name')), Paginator::class), E_USER_DEPRECATED);
71-
72-
foreach ($legacyArgs as $pos => $arg) {
73-
[
74-
'arg_name' => $argName,
75-
'option_name' => $optionName,
76-
'type' => $type,
77-
'default' => $default,
78-
] = $legacyPaginationArgs[$pos];
79-
80-
if (!((null === $default && null === $arg) || \call_user_func("is_{$type}", $arg))) {
81-
throw new InvalidArgumentException(sprintf('The "$%s" argument is expected to be a %s%s.', $argName, $type, null === $default ? ' or null' : ''));
87+
if (!((null === $arg['default'] && null === $args[$pos]) || \call_user_func("is_{$arg['type']}", $args[$pos]))) {
88+
throw new InvalidArgumentException(sprintf('The "$%s" argument is expected to be a %s%s.', $arg['arg_name'], $arg['type'], null === $arg['default'] ? ' or null' : ''));
8289
}
8390

84-
$paginationOptions[$optionName] = $arg;
91+
$value = $args[$pos];
92+
} else {
93+
$value = $arg['default'];
8594
}
86-
}
8795

88-
$pagination = new Pagination($requestStack, $resourceMetadataFactory, $paginationOptions);
96+
$this->{$arg['arg_name']} = $value;
97+
}
8998
} elseif (!$resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
9099
throw new InvalidArgumentException(sprintf('The "$resourceMetadataFactory" argument is expected to be an implementation of the "%s" interface.', ResourceMetadataFactoryInterface::class));
91100
} elseif (!$pagination instanceof Pagination) {
@@ -100,17 +109,13 @@ public function __construct(ManagerRegistry $managerRegistry, /* ResourceMetadat
100109
/**
101110
* {@inheritdoc}
102111
*/
103-
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context = [])
112+
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
104113
{
105-
if (null === $resourceClass) {
106-
throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
107-
}
108-
109-
if (!$this->pagination->isEnabled($resourceClass, $operationName)) {
114+
if (null === $pagination = $this->getPagination($resourceClass, $operationName, $context)) {
110115
return;
111116
}
112117

113-
[, $offset, $limit] = $this->pagination->getPagination($resourceClass, $operationName);
118+
[$offset, $limit] = $pagination;
114119

115120
$queryBuilder
116121
->setFirstResult($offset)
@@ -122,7 +127,15 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
122127
*/
123128
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
124129
{
125-
return $this->pagination->isEnabled($resourceClass, $operationName);
130+
if (null === $this->requestStack) {
131+
return $this->pagination->isEnabled($resourceClass, $operationName, $context);
132+
}
133+
134+
if (null === $request = $this->requestStack->getCurrentRequest()) {
135+
return false;
136+
}
137+
138+
return $this->isPaginationEnabled($request, $this->resourceMetadataFactory->create($resourceClass), $operationName);
126139
}
127140

128141
/**
@@ -133,14 +146,127 @@ public function getResult(QueryBuilder $queryBuilder, string $resourceClass = nu
133146
$doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder, $resourceClass, $operationName));
134147
$doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
135148

136-
if ($this->pagination->isPartialEnabled($resourceClass, $operationName)) {
149+
if (null === $this->requestStack) {
150+
$isPartialEnabled = $this->pagination->isPartialEnabled($resourceClass, $operationName, $context);
151+
} else {
152+
$isPartialEnabled = $this->isPartialPaginationEnabled(
153+
$this->requestStack->getCurrentRequest(),
154+
null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass),
155+
$operationName
156+
);
157+
}
158+
159+
if ($isPartialEnabled) {
137160
return new class($doctrineOrmPaginator) extends AbstractPaginator {
138161
};
139162
}
140163

141164
return new Paginator($doctrineOrmPaginator);
142165
}
143166

167+
/**
168+
* @throws InvalidArgumentException
169+
*/
170+
private function getPagination(string $resourceClass, ?string $operationName, array $context): ?array
171+
{
172+
$request = null;
173+
if (null !== $this->requestStack && null === $request = $this->requestStack->getCurrentRequest()) {
174+
return null;
175+
}
176+
177+
if (null === $request) {
178+
if (!$this->pagination->isEnabled($resourceClass, $operationName, $context)) {
179+
return null;
180+
}
181+
182+
return \array_slice($this->pagination->getPagination($resourceClass, $operationName, $context), 1);
183+
}
184+
185+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
186+
if (!$this->isPaginationEnabled($request, $resourceMetadata, $operationName)) {
187+
return null;
188+
}
189+
190+
$itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true);
191+
if ($request->attributes->get('_graphql')) {
192+
$collectionArgs = $request->attributes->get('_graphql_collections_args', []);
193+
$itemsPerPage = $collectionArgs[$resourceClass]['first'] ?? $itemsPerPage;
194+
}
195+
196+
if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
197+
$maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'maximum_items_per_page', $this->maximumItemPerPage, true);
198+
$itemsPerPage = (int) $this->getPaginationParameter($request, $this->itemsPerPageParameterName, $itemsPerPage);
199+
$itemsPerPage = (null !== $maxItemsPerPage && $itemsPerPage >= $maxItemsPerPage ? $maxItemsPerPage : $itemsPerPage);
200+
}
201+
202+
if (0 > $itemsPerPage) {
203+
throw new InvalidArgumentException('Item per page parameter should not be less than 0');
204+
}
205+
206+
$page = (int) $this->getPaginationParameter($request, $this->pageParameterName, 1);
207+
208+
if (1 > $page) {
209+
throw new InvalidArgumentException('Page should not be less than 1');
210+
}
211+
212+
if (0 === $itemsPerPage && 1 < $page) {
213+
throw new InvalidArgumentException('Page should not be greater than 1 if itemsPerPage is equal to 0');
214+
}
215+
216+
$firstResult = ($page - 1) * $itemsPerPage;
217+
if ($request->attributes->get('_graphql')) {
218+
$collectionArgs = $request->attributes->get('_graphql_collections_args', []);
219+
if (isset($collectionArgs[$resourceClass]['after'])) {
220+
$after = base64_decode($collectionArgs[$resourceClass]['after'], true);
221+
$firstResult = (int) $after;
222+
$firstResult = false === $after ? $firstResult : ++$firstResult;
223+
}
224+
}
225+
226+
return [$firstResult, $itemsPerPage];
227+
}
228+
229+
private function isPartialPaginationEnabled(Request $request = null, ResourceMetadata $resourceMetadata = null, string $operationName = null): bool
230+
{
231+
$enabled = $this->partial;
232+
$clientEnabled = $this->clientPartial;
233+
234+
if ($resourceMetadata) {
235+
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_partial', $enabled, true);
236+
237+
if ($request) {
238+
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_partial', $clientEnabled, true);
239+
}
240+
}
241+
242+
if ($clientEnabled && $request) {
243+
$enabled = filter_var($this->getPaginationParameter($request, $this->partialParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
244+
}
245+
246+
return $enabled;
247+
}
248+
249+
private function isPaginationEnabled(Request $request, ResourceMetadata $resourceMetadata, string $operationName = null): bool
250+
{
251+
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', $this->enabled, true);
252+
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->clientEnabled, true);
253+
254+
if ($clientEnabled) {
255+
$enabled = filter_var($this->getPaginationParameter($request, $this->enabledParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
256+
}
257+
258+
return $enabled;
259+
}
260+
261+
private function getPaginationParameter(Request $request, string $parameterName, $default = null)
262+
{
263+
if (null !== $paginationAttribute = $request->attributes->get('_api_pagination')) {
264+
return array_key_exists($parameterName, $paginationAttribute) ? $paginationAttribute[$parameterName] : $default;
265+
}
266+
267+
return $request->query->get($parameterName, $default);
268+
}
269+
144270
/**
145271
* Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
146272
*

src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ public function getCollection(string $resourceClass, ?string $operationName = nu
8989
$body['query'] = ['match_all' => new \stdClass()];
9090
}
9191

92-
$limit = $body['size'] = $body['size'] ?? $this->pagination->getLimit($resourceClass, $operationName);
93-
$offset = $body['from'] = $body['from'] ?? $this->pagination->getOffset($resourceClass, $operationName);
92+
$limit = $body['size'] = $body['size'] ?? $this->pagination->getLimit($resourceClass, $operationName, $context);
93+
$offset = $body['from'] = $body['from'] ?? $this->pagination->getOffset($resourceClass, $operationName, $context);
9494

9595
$documents = $this->client->search([
9696
'index' => $documentMetadata->getIndex(),

src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
<service id="ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface" alias="api_platform.subresource_data_provider" />
2929

3030
<service id="api_platform.pagination" class="ApiPlatform\Core\DataProvider\Pagination">
31-
<argument type="service" id="request_stack" />
3231
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
3332
<argument>%api_platform.collection.pagination%</argument>
3433
</service>

0 commit comments

Comments
 (0)