Completed
Pull Request — master (#452)
by Kévin
03:48
created

PaginationExtension   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9
Metric Value
wmc 15
lcom 2
cbo 9
dl 0
loc 133
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A applyToCollection() 0 21 4
A supportsResult() 0 11 2
A getResult() 0 7 1
A isPaginationEnabled() 0 11 2
B useOutputWalkers() 0 35 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
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);
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 output walkers should be used.
122
     *
123
     * @param QueryBuilder $queryBuilder
124
     *
125
     * @return bool
126
     */
127
    private function useOutputWalkers(QueryBuilder $queryBuilder) : bool
128
    {
129
        /*
130
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
131
         *
132
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
133
         */
134
        if (QueryChecker::hasHavingClause($queryBuilder)) {
135
            return true;
136
        }
137
138
        /*
139
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
140
         *
141
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
142
         */
143
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
144
            return true;
145
        }
146
147
        /*
148
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
149
         *
150
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
151
         */
152
        if (
153
            QueryChecker::hasMaxResults($queryBuilder) &&
154
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
155
        ) {
156
            return true;
157
        }
158
159
        // Disable output walkers by default (performance)
160
        return false;
161
    }
162
}
163