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\Bridge\Doctrine\Orm\Extension; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\AbstractPaginator; |
17
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator; |
18
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker; |
19
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
20
|
|
|
use ApiPlatform\Core\Exception\InvalidArgumentException; |
21
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
22
|
|
|
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
23
|
|
|
use Doctrine\Common\Persistence\ManagerRegistry; |
24
|
|
|
use Doctrine\ORM\QueryBuilder; |
25
|
|
|
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator; |
26
|
|
|
use Symfony\Component\HttpFoundation\Request; |
27
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Applies pagination on the Doctrine query for resource collection when enabled. |
31
|
|
|
* |
32
|
|
|
* @author Kévin Dunglas <[email protected]> |
33
|
|
|
* @author Samuel ROZE <[email protected]> |
34
|
|
|
*/ |
35
|
|
|
final class PaginationExtension implements QueryResultCollectionExtensionInterface |
36
|
|
|
{ |
37
|
|
|
private $managerRegistry; |
38
|
|
|
private $requestStack; |
39
|
|
|
private $resourceMetadataFactory; |
40
|
|
|
private $enabled; |
41
|
|
|
private $clientEnabled; |
42
|
|
|
private $clientItemsPerPage; |
43
|
|
|
private $itemsPerPage; |
44
|
|
|
private $pageParameterName; |
45
|
|
|
private $enabledParameterName; |
46
|
|
|
private $itemsPerPageParameterName; |
47
|
|
|
private $maximumItemPerPage; |
48
|
|
|
private $partial; |
49
|
|
|
private $clientPartial; |
50
|
|
|
private $partialParameterName; |
51
|
|
|
|
52
|
|
|
public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, bool $clientEnabled = false, bool $clientItemsPerPage = false, int $itemsPerPage = 30, string $pageParameterName = 'page', string $enabledParameterName = 'pagination', string $itemsPerPageParameterName = 'itemsPerPage', int $maximumItemPerPage = null, bool $partial = false, bool $clientPartial = false, string $partialParameterName = 'partial') |
53
|
|
|
{ |
54
|
|
|
$this->managerRegistry = $managerRegistry; |
55
|
|
|
$this->requestStack = $requestStack; |
56
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
57
|
|
|
$this->enabled = $enabled; |
58
|
|
|
$this->clientEnabled = $clientEnabled; |
59
|
|
|
$this->clientItemsPerPage = $clientItemsPerPage; |
60
|
|
|
$this->itemsPerPage = $itemsPerPage; |
61
|
|
|
$this->pageParameterName = $pageParameterName; |
62
|
|
|
$this->enabledParameterName = $enabledParameterName; |
63
|
|
|
$this->itemsPerPageParameterName = $itemsPerPageParameterName; |
64
|
|
|
$this->maximumItemPerPage = $maximumItemPerPage; |
65
|
|
|
$this->partial = $partial; |
66
|
|
|
$this->clientPartial = $clientPartial; |
67
|
|
|
$this->partialParameterName = $partialParameterName; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* {@inheritdoc} |
72
|
|
|
*/ |
73
|
|
|
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) |
74
|
|
|
{ |
75
|
|
|
$request = $this->requestStack->getCurrentRequest(); |
76
|
|
|
if (null === $request) { |
77
|
|
|
return; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
81
|
|
|
if (!$this->isPaginationEnabled($request, $resourceMetadata, $operationName)) { |
82
|
|
|
return; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
$itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true); |
86
|
|
|
if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) { |
87
|
|
|
$itemsPerPage = (int) $request->query->get($this->itemsPerPageParameterName, $itemsPerPage); |
88
|
|
|
$itemsPerPage = (null !== $this->maximumItemPerPage && $itemsPerPage >= $this->maximumItemPerPage ? $this->maximumItemPerPage : $itemsPerPage); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
if (0 >= $itemsPerPage) { |
92
|
|
|
throw new InvalidArgumentException('Item per page parameter should not be less than or equal to 0'); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$queryBuilder |
96
|
|
|
->setFirstResult(($request->query->get($this->pageParameterName, 1) - 1) * $itemsPerPage) |
97
|
|
|
->setMaxResults($itemsPerPage); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* {@inheritdoc} |
102
|
|
|
*/ |
103
|
|
|
public function supportsResult(string $resourceClass, string $operationName = null): bool |
104
|
|
|
{ |
105
|
|
|
$request = $this->requestStack->getCurrentRequest(); |
106
|
|
|
if (null === $request) { |
107
|
|
|
return false; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
111
|
|
|
|
112
|
|
|
return $this->isPaginationEnabled($request, $resourceMetadata, $operationName); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* {@inheritdoc} |
117
|
|
|
*/ |
118
|
|
|
public function getResult(QueryBuilder $queryBuilder/*, string $resourceClass, string $operationName = null*/) |
|
|
|
|
119
|
|
|
{ |
120
|
|
|
$resourceClass = $operationName = null; |
121
|
|
|
|
122
|
|
View Code Duplication |
if (func_num_args() >= 2) { |
|
|
|
|
123
|
|
|
$resourceClass = func_get_arg(1); |
124
|
|
|
} else { |
125
|
|
|
@trigger_error(sprintf('Method %s() will have a 2nd `string $resourceClass` argument in version 3.0. Not defining it is deprecated since 2.2.', __METHOD__), E_USER_DEPRECATED); |
|
|
|
|
126
|
|
|
} |
127
|
|
|
|
128
|
|
View Code Duplication |
if (func_num_args() >= 3) { |
|
|
|
|
129
|
|
|
$operationName = func_get_arg(2); |
130
|
|
|
} else { |
131
|
|
|
@trigger_error(sprintf('Method %s() will have a 3rd `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.2.', __METHOD__), E_USER_DEPRECATED); |
|
|
|
|
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder)); |
135
|
|
|
$doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder)); |
136
|
|
|
|
137
|
|
|
$resourceMetadata = null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass); |
138
|
|
|
|
139
|
|
|
if ($this->isPartialPaginationEnabled($this->requestStack->getCurrentRequest(), $resourceMetadata, $operationName)) { |
140
|
|
|
return new class($doctrineOrmPaginator) extends AbstractPaginator { |
141
|
|
|
}; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
return new Paginator($doctrineOrmPaginator); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
private function isPartialPaginationEnabled(Request $request = null, ResourceMetadata $resourceMetadata = null, string $operationName = null): bool |
148
|
|
|
{ |
149
|
|
|
$enabled = $this->partial; |
150
|
|
|
$clientEnabled = $this->clientPartial; |
151
|
|
|
|
152
|
|
|
if ($resourceMetadata) { |
153
|
|
|
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_partial', $enabled, true); |
154
|
|
|
|
155
|
|
|
if ($request) { |
156
|
|
|
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_partial', $clientEnabled, true); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
if ($clientEnabled && $request) { |
161
|
|
|
$enabled = filter_var($request->query->get($this->partialParameterName, $enabled), FILTER_VALIDATE_BOOLEAN); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return $enabled; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
private function isPaginationEnabled(Request $request, ResourceMetadata $resourceMetadata, string $operationName = null): bool |
168
|
|
|
{ |
169
|
|
|
$enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', $this->enabled, true); |
170
|
|
|
$clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->clientEnabled, true); |
171
|
|
|
|
172
|
|
|
if ($clientEnabled) { |
173
|
|
|
$enabled = filter_var($request->query->get($this->enabledParameterName, $enabled), FILTER_VALIDATE_BOOLEAN); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
return $enabled; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not. |
181
|
|
|
* |
182
|
|
|
* @see https://github.com/doctrine/doctrine2/issues/2910 |
183
|
|
|
* |
184
|
|
|
* @param QueryBuilder $queryBuilder |
185
|
|
|
* |
186
|
|
|
* @return bool |
187
|
|
|
*/ |
188
|
|
|
private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool |
189
|
|
|
{ |
190
|
|
|
return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Determines whether output walkers should be used. |
195
|
|
|
* |
196
|
|
|
* @param QueryBuilder $queryBuilder |
197
|
|
|
* |
198
|
|
|
* @return bool |
199
|
|
|
*/ |
200
|
|
|
private function useOutputWalkers(QueryBuilder $queryBuilder): bool |
201
|
|
|
{ |
202
|
|
|
/* |
203
|
|
|
* "Cannot count query that uses a HAVING clause. Use the output walkers for pagination" |
204
|
|
|
* |
205
|
|
|
* @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50 |
206
|
|
|
*/ |
207
|
|
|
if (QueryChecker::hasHavingClause($queryBuilder)) { |
208
|
|
|
return true; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/* |
212
|
|
|
* "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator." |
213
|
|
|
* |
214
|
|
|
* @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87 |
215
|
|
|
*/ |
216
|
|
|
if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) { |
217
|
|
|
return true; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/* |
221
|
|
|
* "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers." |
222
|
|
|
* |
223
|
|
|
* @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149 |
224
|
|
|
*/ |
225
|
|
|
if ( |
226
|
|
|
QueryChecker::hasMaxResults($queryBuilder) && |
227
|
|
|
QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry) |
228
|
|
|
) { |
229
|
|
|
return true; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/* |
233
|
|
|
* When using composite identifiers pagination will need Output walkers |
234
|
|
|
*/ |
235
|
|
|
if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) { |
236
|
|
|
return true; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
// Disable output walkers by default (performance) |
240
|
|
|
return false; |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
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.