Completed
Pull Request — master (#1619)
by Kévin
03:14
created

PaginationExtension::applyToCollection()   C

Complexity

Conditions 14
Paths 63

Size

Total Lines 51
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 51
rs 5.6426
c 0
b 0
f 0
cc 14
eloc 31
nc 63
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 ContextAwareQueryResultCollectionExtensionInterface
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 = null, string $operationName = null, array $context = [])
74
    {
75
        if (null === $resourceClass) {
76
            throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
77
        }
78
79
        $request = $this->requestStack->getCurrentRequest();
80
        if (null === $request) {
81
            return;
82
        }
83
84
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
85
        if (!$this->isPaginationEnabled($request, $resourceMetadata, $operationName)) {
86
            return;
87
        }
88
89
        $itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true);
90
        if ($request->attributes->get('_graphql')) {
91
            $collectionArgs = $request->attributes->get('_graphql_collections_args', []);
92
            $itemsPerPage = $collectionArgs[$resourceClass]['first'] ?? $itemsPerPage;
93
        }
94
95
        if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
96
            $itemsPerPage = (int) $this->getPaginationParameter($request, $this->itemsPerPageParameterName, $itemsPerPage);
97
            $itemsPerPage = (null !== $this->maximumItemPerPage && $itemsPerPage >= $this->maximumItemPerPage ? $this->maximumItemPerPage : $itemsPerPage);
98
        }
99
100
        if (0 > $itemsPerPage) {
101
            throw new InvalidArgumentException('Item per page parameter should not be less than 0');
102
        }
103
104
        $page = $this->getPaginationParameter($request, $this->pageParameterName, 1);
105
106
        if (0 === $itemsPerPage && 1 < $page) {
107
            throw new InvalidArgumentException('Page should not be greater than 1 if itemsPegPage is equal to 0');
108
        }
109
110
        $firstResult = ($page - 1) * $itemsPerPage;
111
        if ($request->attributes->get('_graphql')) {
112
            $collectionArgs = $request->attributes->get('_graphql_collections_args', []);
113
            if (isset($collectionArgs[$resourceClass]['after'])) {
114
                $after = \base64_decode($collectionArgs[$resourceClass]['after'], true);
115
                $firstResult = (int) $after;
116
                $firstResult = false === $after ? $firstResult : ++$firstResult;
117
            }
118
        }
119
120
        $queryBuilder
121
            ->setFirstResult($firstResult)
122
            ->setMaxResults($itemsPerPage);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
129
    {
130
        $request = $this->requestStack->getCurrentRequest();
131
        if (null === $request) {
132
            return false;
133
        }
134
135
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
136
137
        return $this->isPaginationEnabled($request, $resourceMetadata, $operationName);
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = [])
144
    {
145
        $doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder));
146
        $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
147
148
        $resourceMetadata = null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass);
149
150
        if ($this->isPartialPaginationEnabled($this->requestStack->getCurrentRequest(), $resourceMetadata, $operationName)) {
151
            return new class($doctrineOrmPaginator) extends AbstractPaginator {
152
            };
153
        }
154
155
        return new Paginator($doctrineOrmPaginator);
156
    }
157
158
    private function isPartialPaginationEnabled(Request $request = null, ResourceMetadata $resourceMetadata = null, string $operationName = null): bool
159
    {
160
        $enabled = $this->partial;
161
        $clientEnabled = $this->clientPartial;
162
163
        if ($resourceMetadata) {
164
            $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_partial', $enabled, true);
165
166
            if ($request) {
167
                $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_partial', $clientEnabled, true);
168
            }
169
        }
170
171
        if ($clientEnabled && $request) {
172
            $enabled = filter_var($this->getPaginationParameter($request, $this->partialParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
173
        }
174
175
        return $enabled;
176
    }
177
178
    private function isPaginationEnabled(Request $request, ResourceMetadata $resourceMetadata, string $operationName = null): bool
179
    {
180
        $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', $this->enabled, true);
181
        $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->clientEnabled, true);
182
183
        if ($clientEnabled) {
184
            $enabled = filter_var($this->getPaginationParameter($request, $this->enabledParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
185
        }
186
187
        return $enabled;
188
    }
189
190
    /**
191
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
192
     *
193
     * @see https://github.com/doctrine/doctrine2/issues/2910
194
     *
195
     * @param QueryBuilder $queryBuilder
196
     *
197
     * @return bool
198
     */
199
    private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool
200
    {
201
        return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry);
202
    }
203
204
    /**
205
     * Determines whether output walkers should be used.
206
     *
207
     * @param QueryBuilder $queryBuilder
208
     *
209
     * @return bool
210
     */
211
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
212
    {
213
        /*
214
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
215
         *
216
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
217
         */
218
        if (QueryChecker::hasHavingClause($queryBuilder)) {
219
            return true;
220
        }
221
222
        /*
223
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
224
         *
225
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
226
         */
227
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
228
            return true;
229
        }
230
231
        /*
232
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
233
         *
234
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
235
         */
236
        if (
237
            QueryChecker::hasMaxResults($queryBuilder) &&
238
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
239
        ) {
240
            return true;
241
        }
242
243
        /*
244
         * When using composite identifiers pagination will need Output walkers
245
         */
246
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
247
            return true;
248
        }
249
250
        // Disable output walkers by default (performance)
251
        return false;
252
    }
253
254
    private function getPaginationParameter(Request $request, string $parameterName, $default = null)
255
    {
256
        if (null !== $paginationAttribute = $request->attributes->get('_api_pagination')) {
257
            return array_key_exists($parameterName, $paginationAttribute) ? $paginationAttribute[$parameterName] : $default;
258
        }
259
260
        return $request->query->get($parameterName, $default);
261
    }
262
}
263