Test Failed
Branch master (ec8ced)
by Kévin
11:20
created

PaginationExtension::getResult()

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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