Completed
Pull Request — master (#706)
by Amrouche
03:16
created

PaginationExtension::getResult()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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