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

ResultProvider::getResultForQuery()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.9256

Importance

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