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