Completed
Pull Request — 2.0 (#1143)
by Amrouche
03:34
created

PaginationExtension::isPaginationEnabled()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 6
nc 2
nop 3
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 or equal to 0');
86
        }
87
88
        $queryBuilder
89
            ->setFirstResult(($request->query->get($this->pageParameterName, 1) - 1) * $itemsPerPage)
90
            ->setMaxResults($itemsPerPage);
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function supportsResult(string $resourceClass, string $operationName = null): bool
97
    {
98
        $request = $this->requestStack->getCurrentRequest();
99
        if (null === $request) {
100
            return false;
101
        }
102
103
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
104
105
        return $this->isPaginationEnabled($request, $resourceMetadata, $operationName);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function getResult(QueryBuilder $queryBuilder)
112
    {
113
        $doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder));
114
        $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
115
116
        return new Paginator($doctrineOrmPaginator);
117
    }
118
119
    private function isPaginationEnabled(Request $request, ResourceMetadata $resourceMetadata, string $operationName = null): bool
120
    {
121
        $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', $this->enabled, true);
122
        $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->clientEnabled, true);
123
124
        if ($clientEnabled) {
125
            $enabled = filter_var($request->query->get($this->enabledParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
126
        }
127
128
        return $enabled;
129
    }
130
131
    /**
132
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
133
     *
134
     * @see https://github.com/doctrine/doctrine2/issues/2910
135
     *
136
     * @param QueryBuilder $queryBuilder
137
     *
138
     * @return bool
139
     */
140
    private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool
141
    {
142
        return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry);
143
    }
144
145
    /**
146
     * Determines whether output walkers should be used.
147
     *
148
     * @param QueryBuilder $queryBuilder
149
     *
150
     * @return bool
151
     */
152
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
153
    {
154
        /*
155
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
156
         *
157
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
158
         */
159
        if (QueryChecker::hasHavingClause($queryBuilder)) {
160
            return true;
161
        }
162
163
        /*
164
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
165
         *
166
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
167
         */
168
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
169
            return true;
170
        }
171
172
        /*
173
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
174
         *
175
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
176
         */
177
        if (
178
            QueryChecker::hasMaxResults($queryBuilder) &&
179
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
180
        ) {
181
            return true;
182
        }
183
184
        /*
185
         * When using composite identifiers pagination will need Output walkers
186
         */
187
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
188
            return true;
189
        }
190
191
        // Disable output walkers by default (performance)
192
        return false;
193
    }
194
}
195