Passed
Push — master ( cde1e0...39b8c1 )
by Marius
02:01
created

ResultProvider::getTotalCountForQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Paysera\Pagination\Service\Doctrine;
5
6
use Doctrine\ORM\Query\Expr;
7
use Doctrine\ORM\Query\Expr\Andx;
8
use Doctrine\ORM\Query\Expr\Orx;
9
use Doctrine\ORM\Query\Expr\Comparison;
10
use Doctrine\ORM\QueryBuilder;
11
use Paysera\Pagination\Entity\Doctrine\AnalysedQuery;
12
use Paysera\Pagination\Entity\OrderingConfiguration;
13
use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery;
14
use Paysera\Pagination\Entity\Pager;
15
use Paysera\Pagination\Entity\Result;
16
use Paysera\Pagination\Service\CursorBuilderInterface;
17
18
class ResultProvider
19
{
20
    private $queryAnalyser;
21
    private $cursorBuilder;
22
23 33
    public function __construct(QueryAnalyser $queryAnalyser, CursorBuilderInterface $cursorBuilder)
24
    {
25 33
        $this->queryAnalyser = $queryAnalyser;
26 33
        $this->cursorBuilder = $cursorBuilder;
27 33
    }
28
29 31
    public function getResultForQuery(ConfiguredQuery $configuredQuery, Pager $pager): Result
30
    {
31 31
        $analysedQuery = $this->queryAnalyser->analyseQuery($configuredQuery, $pager);
32
33 30
        $result = $this->buildResult($analysedQuery, $pager);
34
35 30
        if ($configuredQuery->isTotalCountNeeded()) {
36 2
            $totalCount = $this->calculateTotalCount($pager, count($result->getItems()));
37 2
            if ($totalCount === null) {
38 1
                $totalCount = $this->findCount($analysedQuery);
39
            }
40 2
            $result->setTotalCount($totalCount);
41
        }
42
43 30
        return $result;
44
    }
45
46 2
    public function getTotalCountForQuery(ConfiguredQuery $configuredQuery): int
47
    {
48 2
        $analysedQuery = $this->queryAnalyser->analyseQueryWithoutPager($configuredQuery);
49
50 2
        return $this->findCount($analysedQuery);
51
    }
52
53 30
    private function buildResult(AnalysedQuery $analysedQuery, Pager $pager)
54
    {
55 30
        $items = $this->findItems($analysedQuery, $pager);
56
57 30
        if (count($items) === 0) {
58 11
            return $this->buildResultForEmptyItems($analysedQuery, $pager);
59
        }
60
61 20
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
62 20
        $previousCursor = $this->cursorBuilder->getCursorFromItem($items[0], $orderingConfigurations);
63 20
        $nextCursor = $this->cursorBuilder->getCursorFromItem($items[count($items) - 1], $orderingConfigurations);
64
65 20
        return (new Result())
66 20
            ->setItems($items)
67 20
            ->setPreviousCursor($previousCursor)
68 20
            ->setNextCursor($nextCursor)
69 20
            ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
70 20
            ->setHasNext($this->existsAfterCursor($nextCursor, $analysedQuery))
71
        ;
72
    }
73
74 30
    private function findItems(AnalysedQuery $analysedQuery, Pager $pager)
75
    {
76 30
        $pagedQueryBuilder = $this->pageQueryBuilder($analysedQuery, $pager);
77 30
        $query = $pagedQueryBuilder->getQuery();
78 30
        $items = $query->getResult();
79
80 30
        if ($pager->getBefore() !== null) {
81 28
            return array_reverse($items);
82
        }
83 30
        return $items;
84
    }
85
86 30
    private function pageQueryBuilder(AnalysedQuery $analysedQuery, Pager $pager)
87
    {
88 30
        $queryBuilder = $analysedQuery->cloneQueryBuilder();
89
90 30
        if ($pager->getLimit() !== null) {
91 30
            $queryBuilder->setMaxResults($pager->getLimit());
92
        }
93
94 30
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
95 30
        if ($pager->getBefore() !== null) {
96 28
            $orderingConfigurations = $this->reverseOrderingDirection($orderingConfigurations);
97
        }
98
99 30
        $this->applyOrdering($queryBuilder, $orderingConfigurations);
100
101 30
        if ($pager->getOffset() !== null) {
102 6
            $this->applyOffset($queryBuilder, $pager->getOffset());
103 30
        } elseif ($pager->getBefore() !== null) {
104 28
            $this->applyBefore($queryBuilder, $pager->getBefore(), $analysedQuery);
105 30
        } elseif ($pager->getAfter() !== null) {
106 27
            $this->applyAfter($queryBuilder, $pager->getAfter(), $analysedQuery);
107
        }
108
109 30
        return $queryBuilder;
110
    }
111
112
    /**
113
     * @param QueryBuilder $queryBuilder
114
     * @param array|OrderingConfiguration[] $orderingConfigurations
115
     */
116 30
    private function applyOrdering(QueryBuilder $queryBuilder, array $orderingConfigurations)
117
    {
118 30
        foreach ($orderingConfigurations as $orderingConfiguration) {
119 30
            $queryBuilder->addOrderBy(
120 30
                $orderingConfiguration->getOrderByExpression(),
121 30
                $orderingConfiguration->isOrderAscending() ? 'ASC' : 'DESC'
122
            );
123
        }
124 30
    }
125
126 6
    private function applyOffset(QueryBuilder $queryBuilder, int $offset)
127
    {
128 6
        $queryBuilder->setFirstResult($offset);
129 6
    }
130
131
    /**
132
     * @param QueryBuilder $queryBuilder
133
     * @param string $after
134
     * @param AnalysedQuery $analysedQuery
135
     */
136 27
    private function applyAfter(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
137
    {
138 27
        $this->applyCursor($queryBuilder, $after, $analysedQuery, false);
139 27
    }
140
141
    /**
142
     * @param QueryBuilder $queryBuilder
143
     * @param string $after
144
     * @param AnalysedQuery $analysedQuery
145
     */
146 28
    private function applyBefore(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
147
    {
148 28
        $this->applyCursor($queryBuilder, $after, $analysedQuery, true);
149 28
    }
150
151
    /**
152
     * @param QueryBuilder $queryBuilder
153
     * @param string $cursor
154
     * @param AnalysedQuery $analysedQuery
155
     * @param bool $invert
156
     */
157 28
    private function applyCursor(QueryBuilder $queryBuilder, string $cursor, AnalysedQuery $analysedQuery, bool $invert)
158
    {
159 28
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
160 28
        $parsedCursor = $this->cursorBuilder->parseCursor($cursor, count($orderingConfigurations));
161
162 28
        $expr = new Expr();
163 28
        $whereClause = new Orx();
164 28
        $previousConditions = new Andx();
165 28
        foreach ($orderingConfigurations as $index => $orderingConfiguration) {
166 28
            $useLargerThan = $orderingConfiguration->isOrderAscending();
167 28
            if ($invert) {
168 28
                $useLargerThan = !$useLargerThan;
169
            }
170 28
            $sign = $useLargerThan ? '>' : '<';
171 28
            if ($parsedCursor->isCursoredItemIncluded() && $index === count($orderingConfigurations) - 1) {
172 11
                $sign .= '=';
173
            }
174
175 28
            $currentCondition = clone $previousConditions;
176 28
            $currentCondition->add(new Comparison(
177 28
                $orderingConfiguration->getOrderByExpression(),
178 28
                $sign,
179 28
                $expr->literal($parsedCursor->getElementAtIndex($index))
180
            ));
181
182 28
            $whereClause->add($currentCondition);
183
184 28
            $previousConditions->add(new Comparison(
185 28
                $orderingConfiguration->getOrderByExpression(),
186 28
                '=',
187 28
                $expr->literal($parsedCursor->getElementAtIndex($index))
188
            ));
189
        }
190
191 28
        $queryBuilder->andWhere($whereClause);
192 28
    }
193
194 11
    private function buildResultForEmptyItems(AnalysedQuery $analysedQuery, Pager $pager): Result
195
    {
196 11
        if ($pager->getLimit() === 0) {
197 3
            return $this->buildResultForZeroLimit($analysedQuery, $pager);
198
199 9
        } elseif ($pager->getBefore() !== null) {
200 2
            $nextCursor = $this->cursorBuilder->invertCursorInclusion($pager->getBefore());
201 2
            return (new Result())
202 2
                ->setPreviousCursor($pager->getBefore())
203 2
                ->setNextCursor($nextCursor)
204 2
                ->setHasPrevious(false)
205 2
                ->setHasNext($this->existsAfterCursor($nextCursor, $analysedQuery))
206
            ;
207
208 7
        } elseif ($pager->getAfter() !== null) {
209 5
            $previousCursor = $this->cursorBuilder->invertCursorInclusion($pager->getAfter());
210 5
            return (new Result())
211 5
                ->setPreviousCursor($previousCursor)
212 5
                ->setNextCursor($pager->getAfter())
213 5
                ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
214 5
                ->setHasNext(false)
215
            ;
216
217 3
        } elseif ($pager->getOffset() !== null && $pager->getOffset() > 0) {
218 3
            return $this->buildResultForTooLargeOffset($analysedQuery);
219
220
        }
221
222 1
        return (new Result())
223 1
            ->setHasPrevious(false)
224 1
            ->setHasNext(false)
225
        ;
226
    }
227
228 3
    private function buildResultForZeroLimit(AnalysedQuery $analysedQuery, Pager $zeroLimitPager): Result
229
    {
230 3
        $pager = (clone $zeroLimitPager)->setLimit(1);
231 3
        $items = $this->findItems($analysedQuery, $pager);
232
233 3
        if (count($items) === 0) {
234 1
            return $this->buildResultForEmptyItems($analysedQuery, $pager);
235
        }
236
237 2
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
238 2
        $previousCursor = $this->cursorBuilder->getCursorFromItem($items[0], $orderingConfigurations);
239 2
        $nextCursor = $this->cursorBuilder->buildCursorWithIncludedItem($previousCursor);
240
241 2
        return (new Result())
242 2
            ->setPreviousCursor($previousCursor)
243 2
            ->setNextCursor($nextCursor)
244 2
            ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
245 2
            ->setHasNext(true)
246
        ;
247
248
    }
249
250 3
    private function buildResultForTooLargeOffset(AnalysedQuery $analysedQuery): Result
251
    {
252 3
        $result = (new Result())->setHasNext(false);
253
254 3
        $pagerForLastElement = (new Pager())->setLimit(1);
255 3
        $modifiedAnalysedQuery = (clone $analysedQuery)->setOrderingConfigurations(
256 3
            $this->reverseOrderingDirection($analysedQuery->getOrderingConfigurations())
257
        );
258 3
        $items = $this->findItems($modifiedAnalysedQuery, $pagerForLastElement);
259 3
        if (count($items) === 0) {
260 1
            return $result->setHasPrevious(false);
261
        }
262
263 2
        $lastItemCursor = $this->cursorBuilder->getCursorFromItem($items[0], $modifiedAnalysedQuery->getOrderingConfigurations());
264
        return $result
265 2
            ->setHasPrevious(true)
266 2
            ->setPreviousCursor($this->cursorBuilder->buildCursorWithIncludedItem($lastItemCursor))
267 2
            ->setNextCursor($lastItemCursor)
268
        ;
269
    }
270
271
    /**
272
     * @param array|OrderingConfiguration[] $orderingConfigurations
273
     * @return array|OrderingConfiguration[]
274
     */
275 30
    private function reverseOrderingDirection(array $orderingConfigurations): array
276
    {
277 30
        $reversedOrderingConfigurations = [];
278 30
        foreach ($orderingConfigurations as $orderingConfiguration) {
279 30
            $reversedOrderingConfigurations[] = (clone $orderingConfiguration)
280 30
                ->setOrderAscending(!$orderingConfiguration->isOrderAscending())
281
            ;
282
        }
283 30
        return $reversedOrderingConfigurations;
284
    }
285
286 26 View Code Duplication
    private function existsBeforeCursor(string $previousCursor, AnalysedQuery $analysedQuery)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
287
    {
288 26
        $nextPager = (new Pager())
289 26
            ->setBefore($previousCursor)
290 26
            ->setLimit(1)
291
        ;
292 26
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
293
    }
294
295 22 View Code Duplication
    private function existsAfterCursor(string $nextCursor, AnalysedQuery $analysedQuery)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
296
    {
297 22
        $nextPager = (new Pager())
298 22
            ->setAfter($nextCursor)
299 22
            ->setLimit(1)
300
        ;
301 22
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
302
    }
303
304 2
    private function calculateTotalCount(Pager $filter, int $resultCount)
305
    {
306
        if (
307 2
            $filter->getOffset() !== null
308 2
            && ($filter->getLimit() === null || $resultCount < $filter->getLimit())
309 2
            && ($resultCount !== 0 || $filter->getOffset() === 0)
310
        ) {
311 1
            return $resultCount + $filter->getOffset();
312
        }
313
314 1
        return null;
315
    }
316
317 3
    private function findCount(AnalysedQuery $analysedQuery): int
318
    {
319 3
        $countQueryBuilder = $analysedQuery->cloneQueryBuilder();
320 3
        $countQueryBuilder->select(sprintf('count(%s)', $analysedQuery->getRootAlias()));
321 3
        return (int)$countQueryBuilder->getQuery()->getSingleScalarResult();
322
    }
323
}
324