Completed
Pull Request — master (#8)
by
unknown
02:42
created

ResultProvider::transformResultItems()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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