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