Completed
Pull Request — master (#504)
by Antoine
06:38 queued 03:10
created

PaginationExtension::useOutputWalkers()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 42
Code Lines 12

Duplication

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