Completed
Push — master ( 39b8c1...64acd2 )
by Marius
03:05
created

ResultProvider::applyCursor()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 6

Importance

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