Completed
Push — 2.1 ( bd9aea...9440d7 )
by Kévin
17s
created

PaginationExtension   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 0
Metric Value
wmc 22
lcom 2
cbo 10
dl 0
loc 167
rs 10
c 0
b 0
f 0

7 Methods

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