Passed
Pull Request — master (#9)
by
unknown
03:26
created

ResultProvider::transformResultItems()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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