Completed
Push — master ( 060764...f21b16 )
by
unknown
43s queued 31s
created

PaginationExtension::isPartialPaginationEnabled()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 6
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\AbstractPaginator;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
19
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
20
use ApiPlatform\Core\Exception\InvalidArgumentException;
21
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
23
use Doctrine\Common\Persistence\ManagerRegistry;
24
use Doctrine\ORM\QueryBuilder;
25
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpFoundation\RequestStack;
28
29
/**
30
 * Applies pagination on the Doctrine query for resource collection when enabled.
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 * @author Samuel ROZE <[email protected]>
34
 */
35
final class PaginationExtension implements QueryResultCollectionExtensionInterface
36
{
37
    private $managerRegistry;
38
    private $requestStack;
39
    private $resourceMetadataFactory;
40
    private $enabled;
41
    private $clientEnabled;
42
    private $clientItemsPerPage;
43
    private $itemsPerPage;
44
    private $pageParameterName;
45
    private $enabledParameterName;
46
    private $itemsPerPageParameterName;
47
    private $maximumItemPerPage;
48
    private $partial;
49
    private $clientPartial;
50
    private $partialParameterName;
51
52
    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, bool $partial = false, bool $clientPartial = false, string $partialParameterName = 'partial')
53
    {
54
        $this->managerRegistry = $managerRegistry;
55
        $this->requestStack = $requestStack;
56
        $this->resourceMetadataFactory = $resourceMetadataFactory;
57
        $this->enabled = $enabled;
58
        $this->clientEnabled = $clientEnabled;
59
        $this->clientItemsPerPage = $clientItemsPerPage;
60
        $this->itemsPerPage = $itemsPerPage;
61
        $this->pageParameterName = $pageParameterName;
62
        $this->enabledParameterName = $enabledParameterName;
63
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
64
        $this->maximumItemPerPage = $maximumItemPerPage;
65
        $this->partial = $partial;
66
        $this->clientPartial = $clientPartial;
67
        $this->partialParameterName = $partialParameterName;
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
74
    {
75
        $request = $this->requestStack->getCurrentRequest();
76
        if (null === $request) {
77
            return;
78
        }
79
80
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
81
        if (!$this->isPaginationEnabled($request, $resourceMetadata, $operationName)) {
82
            return;
83
        }
84
85
        $itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true);
86
        if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
87
            $itemsPerPage = (int) $request->query->get($this->itemsPerPageParameterName, $itemsPerPage);
88
            $itemsPerPage = (null !== $this->maximumItemPerPage && $itemsPerPage >= $this->maximumItemPerPage ? $this->maximumItemPerPage : $itemsPerPage);
89
        }
90
91
        if (0 >= $itemsPerPage) {
92
            throw new InvalidArgumentException('Item per page parameter should not be less than or equal to 0');
93
        }
94
95
        $queryBuilder
96
            ->setFirstResult(($request->query->get($this->pageParameterName, 1) - 1) * $itemsPerPage)
97
            ->setMaxResults($itemsPerPage);
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function supportsResult(string $resourceClass, string $operationName = null): bool
104
    {
105
        $request = $this->requestStack->getCurrentRequest();
106
        if (null === $request) {
107
            return false;
108
        }
109
110
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
111
112
        return $this->isPaginationEnabled($request, $resourceMetadata, $operationName);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function getResult(QueryBuilder $queryBuilder/*, string $resourceClass, string $operationName = null*/)
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
119
    {
120
        $resourceClass = $operationName = null;
121
122 View Code Duplication
        if (func_num_args() >= 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
123
            $resourceClass = func_get_arg(1);
124
        } else {
125
            @trigger_error(sprintf('Method %s() will have a 2nd `string $resourceClass` argument in version 3.0. Not defining it is deprecated since 2.2.', __METHOD__), E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
126
        }
127
128 View Code Duplication
        if (func_num_args() >= 3) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
            $operationName = func_get_arg(2);
130
        } else {
131
            @trigger_error(sprintf('Method %s() will have a 3rd `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.2.', __METHOD__), E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
132
        }
133
134
        $doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder));
135
        $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
136
137
        $resourceMetadata = null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass);
138
139
        if ($this->isPartialPaginationEnabled($this->requestStack->getCurrentRequest(), $resourceMetadata, $operationName)) {
140
            return new class($doctrineOrmPaginator) extends AbstractPaginator {
141
            };
142
        }
143
144
        return new Paginator($doctrineOrmPaginator);
145
    }
146
147
    private function isPartialPaginationEnabled(Request $request = null, ResourceMetadata $resourceMetadata = null, string $operationName = null): bool
148
    {
149
        $enabled = $this->partial;
150
        $clientEnabled = $this->clientPartial;
151
152
        if ($resourceMetadata) {
153
            $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_partial', $enabled, true);
154
155
            if ($request) {
156
                $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_partial', $clientEnabled, true);
157
            }
158
        }
159
160
        if ($clientEnabled && $request) {
161
            $enabled = filter_var($request->query->get($this->partialParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
162
        }
163
164
        return $enabled;
165
    }
166
167
    private function isPaginationEnabled(Request $request, ResourceMetadata $resourceMetadata, string $operationName = null): bool
168
    {
169
        $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', $this->enabled, true);
170
        $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->clientEnabled, true);
171
172
        if ($clientEnabled) {
173
            $enabled = filter_var($request->query->get($this->enabledParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
174
        }
175
176
        return $enabled;
177
    }
178
179
    /**
180
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
181
     *
182
     * @see https://github.com/doctrine/doctrine2/issues/2910
183
     *
184
     * @param QueryBuilder $queryBuilder
185
     *
186
     * @return bool
187
     */
188
    private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool
189
    {
190
        return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry);
191
    }
192
193
    /**
194
     * Determines whether output walkers should be used.
195
     *
196
     * @param QueryBuilder $queryBuilder
197
     *
198
     * @return bool
199
     */
200
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
201
    {
202
        /*
203
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
204
         *
205
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
206
         */
207
        if (QueryChecker::hasHavingClause($queryBuilder)) {
208
            return true;
209
        }
210
211
        /*
212
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
213
         *
214
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
215
         */
216
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
217
            return true;
218
        }
219
220
        /*
221
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
222
         *
223
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
224
         */
225
        if (
226
            QueryChecker::hasMaxResults($queryBuilder) &&
227
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
228
        ) {
229
            return true;
230
        }
231
232
        /*
233
         * When using composite identifiers pagination will need Output walkers
234
         */
235
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
236
            return true;
237
        }
238
239
        // Disable output walkers by default (performance)
240
        return false;
241
    }
242
}
243