Skip to content

Commit e416d09

Browse files
committed
fix: add legacy bridge pagination
1 parent 92fe17c commit e416d09

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
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\Core\DataProvider;
15+
16+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
17+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
18+
use ApiPlatform\Exception\InvalidArgumentException;
19+
use ApiPlatform\Exception\OperationNotFoundException;
20+
use ApiPlatform\Exception\ResourceClassNotFoundException;
21+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
22+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
23+
24+
/**
25+
* Pagination configuration.
26+
*
27+
* @author Baptiste Meyer <[email protected]>
28+
*/
29+
final class Pagination
30+
{
31+
private $options;
32+
private $graphQlOptions;
33+
34+
/**
35+
* @var ResourceMetadataCollectionFactoryInterface|ResourceMetadataFactoryInterface|null
36+
*/
37+
private $resourceMetadataFactory;
38+
39+
public function __construct($resourceMetadataFactory, array $options = [], array $graphQlOptions = [])
40+
{
41+
if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
42+
trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));
43+
}
44+
45+
$this->resourceMetadataFactory = $resourceMetadataFactory;
46+
$this->options = array_merge([
47+
'enabled' => true,
48+
'client_enabled' => false,
49+
'client_items_per_page' => false,
50+
'items_per_page' => 30,
51+
'page_default' => 1,
52+
'page_parameter_name' => 'page',
53+
'enabled_parameter_name' => 'pagination',
54+
'items_per_page_parameter_name' => 'itemsPerPage',
55+
'maximum_items_per_page' => null,
56+
'partial' => false,
57+
'client_partial' => false,
58+
'partial_parameter_name' => 'partial',
59+
], $options);
60+
$this->graphQlOptions = array_merge([
61+
'enabled' => true,
62+
], $graphQlOptions);
63+
}
64+
65+
/**
66+
* Gets the current page.
67+
*
68+
* @throws InvalidArgumentException
69+
*/
70+
public function getPage(array $context = []): int
71+
{
72+
$page = (int) $this->getParameterFromContext(
73+
$context,
74+
$this->options['page_parameter_name'],
75+
$this->options['page_default']
76+
);
77+
78+
if (1 > $page) {
79+
throw new InvalidArgumentException('Page should not be less than 1');
80+
}
81+
82+
return $page;
83+
}
84+
85+
/**
86+
* Gets the current offset.
87+
*/
88+
public function getOffset(string $resourceClass = null, string $operationName = null, array $context = []): int
89+
{
90+
$graphql = (bool) ($context['graphql_operation_name'] ?? false);
91+
92+
$limit = $this->getLimit($resourceClass, $operationName, $context);
93+
94+
if ($graphql && null !== ($after = $this->getParameterFromContext($context, 'after'))) {
95+
return false === ($after = base64_decode($after, true)) ? 0 : (int) $after + 1;
96+
}
97+
98+
if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before'))) {
99+
return ($offset = (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit)) < 0 ? 0 : $offset;
100+
}
101+
102+
if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) {
103+
return ($offset = ($context['count'] ?? 0) - $last) < 0 ? 0 : $offset;
104+
}
105+
106+
$offset = ($this->getPage($context) - 1) * $limit;
107+
108+
if (!\is_int($offset)) {
109+
throw new InvalidArgumentException('Page parameter is too large.');
110+
}
111+
112+
return $offset;
113+
}
114+
115+
/**
116+
* Gets the current limit.
117+
*
118+
* @throws InvalidArgumentException
119+
*/
120+
public function getLimit(string $resourceClass = null, string $operationName = null, array $context = []): int
121+
{
122+
$graphql = (bool) ($context['graphql_operation_name'] ?? false);
123+
124+
$limit = $this->options['items_per_page'];
125+
$clientLimit = $this->options['client_items_per_page'];
126+
127+
$resourceMetadata = null;
128+
if (null !== $resourceClass) {
129+
/**
130+
* @var ResourceMetadata|ResourceMetadataCollection
131+
*/
132+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
133+
134+
if ($resourceMetadata instanceof ResourceMetadata) {
135+
$limit = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $limit, true);
136+
$clientLimit = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $clientLimit, true);
137+
} else {
138+
try {
139+
$operation = $resourceMetadata->getOperation($operationName);
140+
$limit = $operation->getPaginationItemsPerPage() ?? $limit;
141+
$clientLimit = $operation->getPaginationClientItemsPerPage() ?? $clientLimit;
142+
} catch (OperationNotFoundException $e) {
143+
// GraphQl operation may not exist
144+
}
145+
}
146+
}
147+
148+
if ($graphql && null !== ($first = $this->getParameterFromContext($context, 'first'))) {
149+
$limit = $first;
150+
}
151+
152+
if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) {
153+
$limit = $last;
154+
}
155+
156+
if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before'))
157+
&& (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit) < 0) {
158+
$limit = (int) $before;
159+
}
160+
161+
if ($clientLimit) {
162+
$limit = (int) $this->getParameterFromContext($context, $this->options['items_per_page_parameter_name'], $limit);
163+
$maxItemsPerPage = null;
164+
165+
if ($resourceMetadata instanceof ResourceMetadata) {
166+
$maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'maximum_items_per_page', null, true);
167+
if (null !== $maxItemsPerPage) {
168+
@trigger_error('The "maximum_items_per_page" option has been deprecated since API Platform 2.5 in favor of "pagination_maximum_items_per_page" and will be removed in API Platform 3.', \E_USER_DEPRECATED);
169+
}
170+
$maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_maximum_items_per_page', $maxItemsPerPage ?? $this->options['maximum_items_per_page'], true);
171+
} elseif ($resourceMetadata instanceof ResourceMetadataCollection) {
172+
try {
173+
$operation = $resourceMetadata->getOperation($operationName);
174+
$maxItemsPerPage = $operation->getPaginationMaximumItemsPerPage() ?? $this->options['maximum_items_per_page'];
175+
} catch (OperationNotFoundException $e) {
176+
$maxItemsPerPage = $this->options['maximum_items_per_page'];
177+
}
178+
}
179+
180+
if (null !== $maxItemsPerPage && $limit > $maxItemsPerPage) {
181+
$limit = $maxItemsPerPage;
182+
}
183+
}
184+
185+
if (0 > $limit) {
186+
throw new InvalidArgumentException('Limit should not be less than 0');
187+
}
188+
189+
return $limit;
190+
}
191+
192+
/**
193+
* Gets info about the pagination.
194+
*
195+
* Returns an array with the following info as values:
196+
* - the page {@see Pagination::getPage()}
197+
* - the offset {@see Pagination::getOffset()}
198+
* - the limit {@see Pagination::getLimit()}
199+
*
200+
* @throws InvalidArgumentException
201+
*/
202+
public function getPagination(string $resourceClass = null, string $operationName = null, array $context = []): array
203+
{
204+
$page = $this->getPage($context);
205+
$limit = $this->getLimit($resourceClass, $operationName, $context);
206+
207+
if (0 === $limit && 1 < $page) {
208+
throw new InvalidArgumentException('Page should not be greater than 1 if limit is equal to 0');
209+
}
210+
211+
return [$page, $this->getOffset($resourceClass, $operationName, $context), $limit];
212+
}
213+
214+
/**
215+
* Is the pagination enabled?
216+
*/
217+
public function isEnabled(string $resourceClass = null, string $operationName = null, array $context = []): bool
218+
{
219+
return $this->getEnabled($context, $resourceClass, $operationName);
220+
}
221+
222+
/**
223+
* Is the pagination enabled for GraphQL?
224+
*/
225+
public function isGraphQlEnabled(?string $resourceClass = null, ?string $operationName = null, array $context = []): bool
226+
{
227+
return $this->getGraphQlEnabled($resourceClass, $operationName);
228+
}
229+
230+
/**
231+
* Is the partial pagination enabled?
232+
*/
233+
public function isPartialEnabled(string $resourceClass = null, string $operationName = null, array $context = []): bool
234+
{
235+
return $this->getEnabled($context, $resourceClass, $operationName, true);
236+
}
237+
238+
public function getOptions(): array
239+
{
240+
return $this->options;
241+
}
242+
243+
public function getGraphQlPaginationType(string $resourceClass, string $operationName): string
244+
{
245+
try {
246+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
247+
248+
if ($resourceMetadata instanceof ResourceMetadataCollection) {
249+
$operation = $resourceMetadata->getOperation($operationName);
250+
251+
return $operation->getPaginationType() ?? 'cursor';
252+
}
253+
} catch (ResourceClassNotFoundException $e) {
254+
return 'cursor';
255+
} catch (OperationNotFoundException $e) {
256+
return 'cursor';
257+
}
258+
259+
return (string) $resourceMetadata->getGraphqlAttribute($operationName, 'pagination_type', 'cursor', true);
260+
}
261+
262+
/**
263+
* Is the classic or partial pagination enabled?
264+
*/
265+
private function getEnabled(array $context, string $resourceClass = null, string $operationName = null, bool $partial = false): bool
266+
{
267+
$enabled = $this->options[$partial ? 'partial' : 'enabled'];
268+
$clientEnabled = $this->options[$partial ? 'client_partial' : 'client_enabled'];
269+
270+
if (null !== $resourceClass) {
271+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
272+
273+
if ($resourceMetadata instanceof ResourceMetadataCollection) {
274+
try {
275+
$operation = $resourceMetadata->getOperation($operationName);
276+
$enabled = ($partial ? $operation->getPaginationPartial() : $operation->getPaginationEnabled()) ?? $enabled;
277+
$clientEnabled = ($partial ? $operation->getPaginationClientPartial() : $operation->getPaginationClientEnabled()) ?? $clientEnabled;
278+
} catch (OperationNotFoundException $e) {
279+
// GraphQl operation may not exist
280+
}
281+
} else {
282+
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, $partial ? 'pagination_partial' : 'pagination_enabled', $enabled, true);
283+
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, $partial ? 'pagination_client_partial' : 'pagination_client_enabled', $clientEnabled, true);
284+
}
285+
}
286+
287+
if ($clientEnabled) {
288+
return filter_var($this->getParameterFromContext($context, $this->options[$partial ? 'partial_parameter_name' : 'enabled_parameter_name'], $enabled), \FILTER_VALIDATE_BOOLEAN);
289+
}
290+
291+
return (bool) $enabled;
292+
}
293+
294+
private function getGraphQlEnabled(?string $resourceClass, ?string $operationName): bool
295+
{
296+
$enabled = $this->graphQlOptions['enabled'];
297+
298+
if (null !== $resourceClass) {
299+
try {
300+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
301+
302+
if ($resourceMetadata instanceof ResourceMetadataCollection) {
303+
$operation = $resourceMetadata->getOperation($operationName);
304+
305+
return $operation->getPaginationEnabled() ?? $enabled;
306+
}
307+
} catch (ResourceClassNotFoundException $e) {
308+
return $enabled;
309+
} catch (OperationNotFoundException $e) {
310+
return $enabled;
311+
}
312+
313+
return (bool) $resourceMetadata->getGraphqlAttribute($operationName, 'pagination_enabled', $enabled, true);
314+
}
315+
316+
return $enabled;
317+
}
318+
319+
/**
320+
* Gets the given pagination parameter name from the given context.
321+
*
322+
* @param mixed|null $default
323+
*/
324+
private function getParameterFromContext(array $context, string $parameterName, $default = null)
325+
{
326+
$filters = $context['filters'] ?? [];
327+
328+
return \array_key_exists($parameterName, $filters) ? $filters[$parameterName] : $default;
329+
}
330+
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,9 @@ private function registerLegacyServices(ContainerBuilder $container, array $conf
839839
$container->removeAlias('api_platform.graphql.schema_builder');
840840
$container->setAlias('api_platform.graphql.schema_builder', 'api_platform.graphql.schema_builder.legacy');
841841

842+
$container->removeDefinition('api_platform.pagination');
843+
$container->setDefinition('api_platform.pagination', $container->getDefinition('api_platform.pagination.legacy'));
844+
842845
foreach ([
843846
'api_platform.metadata.property.metadata_factory.serializer',
844847
'api_platform.route_loader',

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
</service>
3535
<service id="ApiPlatform\State\Pagination\Pagination" alias="api_platform.pagination" />
3636

37+
<service id="api_platform.pagination.legacy" class="ApiPlatform\Core\DataProvider\Pagination">
38+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
39+
<argument>%api_platform.collection.pagination%</argument>
40+
<argument>%api_platform.graphql.collection.pagination%</argument>
41+
</service>
42+
<service id="ApiPlatform\Core\DataProvider\Pagination" alias="api_platform.pagination.legacy" />
43+
3744
<service id="api_platform.pagination_options" class="ApiPlatform\State\Pagination\PaginationOptions">
3845
<argument>%api_platform.collection.pagination.enabled%</argument>
3946
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>

0 commit comments

Comments
 (0)