Passed
Push — master ( 6d13f3...89c0f3 )
by
unknown
03:48
created

PaginationExtension::applyToCollection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 15
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 5
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|RequestStack $resourceMetadataFactory
42
     * @param Pagination|ResourceMetadataFactoryInterface   $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.4 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.4 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.4 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 (!((null === $default && null === $arg) || \call_user_func("is_{$type}", $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
        [, $offset, $limit] = $this->pagination->getPagination($resourceClass, $operationName);
114
115
        $queryBuilder
116
            ->setFirstResult($offset)
117
            ->setMaxResults($limit);
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool
124
    {
125
        return $this->pagination->isEnabled($resourceClass, $operationName);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = [])
132
    {
133
        $doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder, $resourceClass, $operationName));
134
        $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
135
136
        if ($this->pagination->isPartialEnabled($resourceClass, $operationName)) {
137
            return new class($doctrineOrmPaginator) extends AbstractPaginator {
138
            };
139
        }
140
141
        return new Paginator($doctrineOrmPaginator);
142
    }
143
144
    /**
145
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
146
     *
147
     * @see https://github.com/doctrine/doctrine2/issues/2910
148
     */
149
    private function useFetchJoinCollection(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null): bool
150
    {
151
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
152
            return false;
153
        }
154
155
        if (null === $resourceClass) {
156
            return true;
157
        }
158
159
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
160
161
        return $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_fetch_join_collection', true, true);
162
    }
163
164
    /**
165
     * Determines whether output walkers should be used.
166
     */
167
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
168
    {
169
        /*
170
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
171
         *
172
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
173
         */
174
        if (QueryChecker::hasHavingClause($queryBuilder)) {
175
            return true;
176
        }
177
178
        /*
179
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
180
         *
181
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
182
         */
183
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
184
            return true;
185
        }
186
187
        /*
188
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
189
         *
190
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
191
         */
192
        if (
193
            QueryChecker::hasMaxResults($queryBuilder) &&
194
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
195
        ) {
196
            return true;
197
        }
198
199
        /*
200
         * When using composite identifiers pagination will need Output walkers
201
         */
202
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
203
            return true;
204
        }
205
206
        // Disable output walkers by default (performance)
207
        return false;
208
    }
209
}
210