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\Exception\InvalidArgumentException; |
17
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Pagination configuration. |
21
|
|
|
* |
22
|
|
|
* @author Baptiste Meyer <[email protected]> |
23
|
|
|
*/ |
24
|
|
|
final class Pagination |
25
|
|
|
{ |
26
|
|
|
private $options; |
27
|
|
|
private $resourceMetadataFactory; |
28
|
|
|
|
29
|
|
|
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $options = []) |
30
|
|
|
{ |
31
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
32
|
|
|
$this->options = array_merge([ |
33
|
|
|
'enabled' => true, |
34
|
|
|
'client_enabled' => false, |
35
|
|
|
'client_items_per_page' => false, |
36
|
|
|
'items_per_page' => 30, |
37
|
|
|
'page_default' => 1, |
38
|
|
|
'page_parameter_name' => 'page', |
39
|
|
|
'enabled_parameter_name' => 'pagination', |
40
|
|
|
'items_per_page_parameter_name' => 'itemsPerPage', |
41
|
|
|
'maximum_items_per_page' => null, |
42
|
|
|
'partial' => false, |
43
|
|
|
'client_partial' => false, |
44
|
|
|
'partial_parameter_name' => 'partial', |
45
|
|
|
], $options); |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Gets the current page. |
50
|
|
|
* |
51
|
|
|
* @throws InvalidArgumentException |
52
|
|
|
*/ |
53
|
|
|
public function getPage(array $context = []): int |
54
|
|
|
{ |
55
|
|
|
$page = (int) $this->getParameterFromContext( |
56
|
|
|
$context, |
57
|
|
|
$this->options['page_parameter_name'], |
58
|
|
|
$this->options['page_default'] |
59
|
|
|
); |
60
|
|
|
|
61
|
|
|
if (1 > $page) { |
62
|
|
|
throw new InvalidArgumentException('Page should not be less than 1'); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
return $page; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Gets the current offset. |
70
|
|
|
*/ |
71
|
|
|
public function getOffset(string $resourceClass = null, string $operationName = null, array $context = []): int |
72
|
|
|
{ |
73
|
|
|
$graphql = $context['graphql'] ?? false; |
74
|
|
|
|
75
|
|
|
$limit = $this->getLimit($resourceClass, $operationName, $context); |
76
|
|
|
|
77
|
|
|
if ($graphql && null !== ($after = $this->getParameterFromContext($context, 'after'))) { |
78
|
|
|
return false === ($after = base64_decode($after, true)) ? 0 : (int) $after + 1; // break for UUID? |
|
|
|
|
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before'))) { |
82
|
|
|
return ($offset = (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit)) < 0 ? 0 : $offset; // break for UUID? |
|
|
|
|
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) { |
86
|
|
|
return ($offset = ($context['count'] ?? 0) - $last) < 0 ? 0 : $offset; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return ($this->getPage($context) - 1) * $limit; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Gets the current limit. |
94
|
|
|
* |
95
|
|
|
* @throws InvalidArgumentException |
96
|
|
|
*/ |
97
|
|
|
public function getLimit(string $resourceClass = null, string $operationName = null, array $context = []): int |
98
|
|
|
{ |
99
|
|
|
$graphql = $context['graphql'] ?? false; |
100
|
|
|
|
101
|
|
|
$limit = $this->options['items_per_page']; |
102
|
|
|
$clientLimit = $this->options['client_items_per_page']; |
103
|
|
|
|
104
|
|
|
if (null !== $resourceClass) { |
105
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
106
|
|
|
$limit = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $limit, true); |
107
|
|
|
$clientLimit = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $clientLimit, true); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
if ($graphql && null !== ($first = $this->getParameterFromContext($context, 'first'))) { |
111
|
|
|
$limit = $first; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) { |
115
|
|
|
$limit = $last; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before')) |
119
|
|
|
&& (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit) < 0) { |
120
|
|
|
$limit = (int) $before; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
if ($clientLimit) { |
124
|
|
|
$limit = (int) $this->getParameterFromContext($context, $this->options['items_per_page_parameter_name'], $limit); |
125
|
|
|
$maxItemsPerPage = $this->options['maximum_items_per_page']; |
126
|
|
|
|
127
|
|
|
if (null !== $resourceClass) { |
128
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
129
|
|
|
$maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'maximum_items_per_page', $maxItemsPerPage, true); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
if (null !== $maxItemsPerPage && $limit > $maxItemsPerPage) { |
133
|
|
|
$limit = $maxItemsPerPage; |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
if (0 > $limit) { |
138
|
|
|
throw new InvalidArgumentException('Limit should not be less than 0'); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $limit; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Gets info about the pagination. |
146
|
|
|
* |
147
|
|
|
* Returns an array with the following info as values: |
148
|
|
|
* - the page {@see Pagination::getPage()} |
149
|
|
|
* - the offset {@see Pagination::getOffset()} |
150
|
|
|
* - the limit {@see Pagination::getLimit()} |
151
|
|
|
* |
152
|
|
|
* @throws InvalidArgumentException |
153
|
|
|
*/ |
154
|
|
|
public function getPagination(string $resourceClass = null, string $operationName = null, array $context = []): array |
155
|
|
|
{ |
156
|
|
|
$page = $this->getPage($context); |
157
|
|
|
$limit = $this->getLimit($resourceClass, $operationName, $context); |
158
|
|
|
|
159
|
|
|
if (0 === $limit && 1 < $page) { |
160
|
|
|
throw new InvalidArgumentException('Page should not be greater than 1 if limit is equal to 0'); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
return [$page, $this->getOffset($resourceClass, $operationName, $context), $limit]; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Is the pagination enabled? |
168
|
|
|
*/ |
169
|
|
|
public function isEnabled(string $resourceClass = null, string $operationName = null, array $context = []): bool |
170
|
|
|
{ |
171
|
|
|
return $this->getEnabled($context, $resourceClass, $operationName); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Is the partial pagination enabled? |
176
|
|
|
*/ |
177
|
|
|
public function isPartialEnabled(string $resourceClass = null, string $operationName = null, array $context = []): bool |
178
|
|
|
{ |
179
|
|
|
return $this->getEnabled($context, $resourceClass, $operationName, true); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Is the classic or partial pagination enabled? |
184
|
|
|
*/ |
185
|
|
|
private function getEnabled(array $context, string $resourceClass = null, string $operationName = null, bool $partial = false): bool |
186
|
|
|
{ |
187
|
|
|
$enabled = $this->options[$partial ? 'partial' : 'enabled']; |
188
|
|
|
$clientEnabled = $this->options[$partial ? 'client_partial' : 'client_enabled']; |
189
|
|
|
|
190
|
|
|
if (null !== $resourceClass) { |
191
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
192
|
|
|
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, $partial ? 'pagination_partial' : 'pagination_enabled', $enabled, true); |
193
|
|
|
|
194
|
|
|
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, $partial ? 'pagination_client_partial' : 'pagination_client_enabled', $clientEnabled, true); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
if ($clientEnabled) { |
198
|
|
|
return filter_var($this->getParameterFromContext($context, $this->options[$partial ? 'partial_parameter_name' : 'enabled_parameter_name'], $enabled), FILTER_VALIDATE_BOOLEAN); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
return $enabled; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Gets the given pagination parameter name from the given context. |
206
|
|
|
*/ |
207
|
|
|
private function getParameterFromContext(array $context, string $parameterName, $default = null) |
208
|
|
|
{ |
209
|
|
|
$filters = $context['filters'] ?? []; |
210
|
|
|
|
211
|
|
|
return \array_key_exists($parameterName, $filters) ? $filters[$parameterName] : $default; |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.