Passed
Pull Request — master (#1999)
by Antoine
02:58
created

PaginationExtension::getResult()

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
eloc 5
nc 2
nop 4
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\DataProvider\Pagination;
21
use ApiPlatform\Core\Exception\InvalidArgumentException;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
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\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 ContextAwareQueryResultCollectionExtensionInterface
35
{
36
    private $managerRegistry;
37
    private $resourceMetadataFactory;
38
    private $pagination;
39
40
    /**
41
     * @param ResourceMetadataFactoryInterface $resourceMetadataFactory
42
     * @param Pagination                       $pagination
43
     */
44
    public function __construct(ManagerRegistry $managerRegistry, /* ResourceMetadataFactoryInterface */ $resourceMetadataFactory, /* Pagination */ $pagination)
45
    {
46
        if ($resourceMetadataFactory instanceof RequestStack && $pagination instanceof ResourceMetadataFactoryInterface) {
47
            @trigger_error(sprintf('Passing an instance of "%s" as second argument of "%s" is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.', RequestStack::class, self::class, ResourceMetadataFactoryInterface::class), E_USER_DEPRECATED);
48
            @trigger_error(sprintf('Passing an instance of "%s" as third argument of "%s" is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.', ResourceMetadataFactoryInterface::class, self::class, Pagination::class), E_USER_DEPRECATED);
49
50
            $requestStack = $resourceMetadataFactory;
51
            $resourceMetadataFactory = $pagination;
52
53
            $legacyPaginationArgs = [
54
                3 => ['arg_name' => 'enabled', 'option_name' => 'enabled', 'type' => 'bool', 'default' => true],
55
                4 => ['arg_name' => 'clientEnabled', 'option_name' => 'client_enabled', 'type' => 'bool', 'default' => false],
56
                5 => ['arg_name' => 'clientItemsPerPage', 'option_name' => 'client_items_per_page', 'type' => 'bool', 'default' => false],
57
                6 => ['arg_name' => 'itemsPerPage', 'option_name' => 'items_per_page', 'type' => 'int', 'default' => 30],
58
                7 => ['arg_name' => 'pageParameterName', 'option_name' => 'page_parameter_name', 'type' => 'string', 'default' => 'page'],
59
                8 => ['arg_name' => 'enabledParameterName', 'option_name' => 'enabled_parameter_name', 'type' => 'string', 'default' => 'pagination'],
60
                9 => ['arg_name' => 'itemsPerPageParameterName', 'option_name' => 'items_per_page_parameter_name', 'type' => 'string', 'default' => 'itemsPerPage'],
61
                10 => ['arg_name' => 'maximumItemPerPage', 'option_name' => 'maximum_items_per_page', 'type' => 'int', 'default' => null],
62
                11 => ['arg_name' => 'partial', 'option_name' => 'partial', 'type' => 'bool', 'default' => false],
63
                12 => ['arg_name' => 'clientPartial', 'option_name' => 'client_partial', 'type' => 'bool', 'default' => false],
64
                13 => ['arg_name' => 'partialParameterName', 'option_name' => 'partial_parameter_name', 'type' => 'string', 'default' => 'partial'],
65
            ];
66
67
            $paginationOptions = array_column($legacyPaginationArgs, 'default', 'option_name');
68
69
            if (0 < \count($legacyArgs = \array_slice(\func_get_args(), 3, null, true))) {
70
                @trigger_error(sprintf('Passing "$%s" arguments is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3. Pass an instance of "%s" as third argument instead.', implode('", "$', array_column($legacyPaginationArgs, 'arg_name')), Paginator::class), E_USER_DEPRECATED);
71
72
                foreach ($legacyArgs as $pos => $arg) {
73
                    [
74
                        'arg_name' => $argName,
75
                        'option_name' => $optionName,
76
                        'type' => $type,
77
                        'default' => $default,
78
                    ] = $legacyPaginationArgs[$pos];
79
80
                    if (!(\call_user_func("is_{$type}", $arg) || null === $default && null === $arg)) {
81
                        throw new InvalidArgumentException(sprintf('The "$%s" argument is expected to be a %s%s.', $argName, $type, null === $default ? ' or null' : ''));
82
                    }
83
84
                    $paginationOptions[$optionName] = $arg;
85
                }
86
            }
87
88
            $pagination = new Pagination($requestStack, $resourceMetadataFactory, $paginationOptions);
89
        } elseif (!$resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
90
            throw new InvalidArgumentException(sprintf('The "$resourceMetadataFactory" argument is expected to be an implementation of the "%s" interface.', ResourceMetadataFactoryInterface::class));
91
        } elseif (!$pagination instanceof Pagination) {
92
            throw new InvalidArgumentException(sprintf('The "$pagination" argument is expected to be an instance of the "%s" class.', Pagination::class));
93
        }
94
95
        $this->managerRegistry = $managerRegistry;
96
        $this->resourceMetadataFactory = $resourceMetadataFactory;
97
        $this->pagination = $pagination;
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context = [])
104
    {
105
        if (null === $resourceClass) {
106
            throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
107
        }
108
109
        if (!$this->pagination->isEnabled($resourceClass, $operationName)) {
110
            return;
111
        }
112
113
        if (0 > $limit = $this->pagination->getLimit($resourceClass, $operationName)) {
114
            throw new InvalidArgumentException('Limit should not be less than 0');
115
        }
116
117
        if (1 > $page = $this->pagination->getPage()) {
118
            throw new InvalidArgumentException('Page should not be less than 1');
119
        }
120
121
        if (0 === $limit && 1 < $page) {
122
            throw new InvalidArgumentException('Page should not be greater than 1 if limit is equal to 0');
123
        }
124
125
        $queryBuilder
126
            ->setFirstResult($this->pagination->getOffset($resourceClass, $operationName))
127
            ->setMaxResults($limit);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
134
    {
135
        return $this->pagination->isEnabled($resourceClass, $operationName);
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = [])
142
    {
143
        $doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder, $resourceClass, $operationName));
144
        $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
145
146
        if ($this->pagination->isPartialEnabled($resourceClass, $operationName)) {
147
            return new class($doctrineOrmPaginator) extends AbstractPaginator {
148
            };
149
        }
150
151
        return new Paginator($doctrineOrmPaginator);
152
    }
153
154
    /**
155
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
156
     *
157
     * @see https://github.com/doctrine/doctrine2/issues/2910
158
     */
159
    private function useFetchJoinCollection(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null): bool
160
    {
161
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
162
            return false;
163
        }
164
165
        if (null === $resourceClass) {
166
            return true;
167
        }
168
169
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
170
171
        return $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_fetch_join_collection', true, true);
172
    }
173
174
    /**
175
     * Determines whether output walkers should be used.
176
     */
177
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
178
    {
179
        /*
180
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
181
         *
182
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
183
         */
184
        if (QueryChecker::hasHavingClause($queryBuilder)) {
185
            return true;
186
        }
187
188
        /*
189
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
190
         *
191
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
192
         */
193
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
194
            return true;
195
        }
196
197
        /*
198
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
199
         *
200
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
201
         */
202
        if (
203
            QueryChecker::hasMaxResults($queryBuilder) &&
204
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
205
        ) {
206
            return true;
207
        }
208
209
        /*
210
         * When using composite identifiers pagination will need Output walkers
211
         */
212
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
213
            return true;
214
        }
215
216
        // Disable output walkers by default (performance)
217
        return false;
218
    }
219
}
220