Completed
Pull Request — master (#9)
by
unknown
03:07
created

ResultProvider   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 399
Duplicated Lines 4.01 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 98.14%

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 15
dl 16
loc 399
ccs 211
cts 215
cp 0.9814
rs 3.44
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getTotalCountForQuery() 0 6 1
A getResultForQuery() 0 20 4
A buildResult() 0 20 2
B pageQueryBuilder() 0 25 6
A applyOrdering() 0 9 3
A applyOffset() 0 4 1
A applyAfter() 0 4 1
A applyBefore() 0 4 1
B applyCursor() 0 36 6
B buildResultForEmptyItems() 0 33 6
A buildResultForZeroLimit() 0 20 2
A buildResultForTooLargeOffset() 0 20 2
A reverseOrderingDirection() 0 10 2
A existsBeforeCursor() 8 9 1
A existsAfterCursor() 8 9 1
A calculateTotalCount() 0 12 6
A findItems() 0 15 3
A findCount() 0 19 3
A findCountWithGroupBy() 0 30 3
A getSingleValidGroupByColumn() 0 25 5
A transformResultItems() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ResultProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResultProvider, and based on these observations, apply Extract Interface, too.

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 43
    public function __construct(QueryAnalyser $queryAnalyser, CursorBuilderInterface $cursorBuilder)
26
    {
27 43
        $this->queryAnalyser = $queryAnalyser;
28 43
        $this->cursorBuilder = $cursorBuilder;
29 43
    }
30
31 37
    public function getResultForQuery(ConfiguredQuery $configuredQuery, Pager $pager): Result
32
    {
33 37
        $analysedQuery = $this->queryAnalyser->analyseQuery($configuredQuery, $pager);
34
35 36
        $result = $this->buildResult($analysedQuery, $pager);
36
37 36
        if ($configuredQuery->getItemTransformer() !== null) {
38 1
            $this->transformResultItems($configuredQuery->getItemTransformer(), $result);
39
        }
40
41 36
        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 36
        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 36
    private function buildResult(AnalysedQuery $analysedQuery, Pager $pager)
60
    {
61 36
        $items = $this->findItems($analysedQuery, $pager);
62
63 36
        if (count($items) === 0) {
64 11
            return $this->buildResultForEmptyItems($analysedQuery, $pager);
65
        }
66
67 26
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
68 26
        $previousCursor = $this->cursorBuilder->getCursorFromItem($items[0], $orderingConfigurations);
69 26
        $nextCursor = $this->cursorBuilder->getCursorFromItem($items[count($items) - 1], $orderingConfigurations);
70
71 26
        return (new Result())
72 26
            ->setItems($items)
73 26
            ->setPreviousCursor($previousCursor)
74 26
            ->setNextCursor($nextCursor)
75 26
            ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
76 26
            ->setHasNext($this->existsAfterCursor($nextCursor, $analysedQuery))
77
        ;
78
    }
79
80 36
    private function findItems(AnalysedQuery $analysedQuery, Pager $pager)
81
    {
82 36
        $pagedQueryBuilder = $this->pageQueryBuilder($analysedQuery, $pager);
83 36
        $query = $pagedQueryBuilder->getQuery();
84 36
        if ($analysedQuery->getQueryModifier() !== null) {
85 1
            $queryModifier = $analysedQuery->getQueryModifier();
86 1
            $query = $queryModifier($query);
87
        }
88 36
        $items = $query->getResult();
89
90 36
        if ($pager->getBefore() !== null) {
91 34
            return array_reverse($items);
92
        }
93 36
        return $items;
94
    }
95
96 36
    private function pageQueryBuilder(AnalysedQuery $analysedQuery, Pager $pager)
97
    {
98 36
        $queryBuilder = $analysedQuery->cloneQueryBuilder();
99
100 36
        if ($pager->getLimit() !== null) {
101 36
            $queryBuilder->setMaxResults($pager->getLimit());
102
        }
103
104 36
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
105 36
        if ($pager->getBefore() !== null) {
106 34
            $orderingConfigurations = $this->reverseOrderingDirection($orderingConfigurations);
107
        }
108
109 36
        $this->applyOrdering($queryBuilder, $orderingConfigurations);
110
111 36
        if ($pager->getOffset() !== null) {
112 6
            $this->applyOffset($queryBuilder, $pager->getOffset());
113 36
        } elseif ($pager->getBefore() !== null) {
114 34
            $this->applyBefore($queryBuilder, $pager->getBefore(), $analysedQuery);
115 36
        } elseif ($pager->getAfter() !== null) {
116 33
            $this->applyAfter($queryBuilder, $pager->getAfter(), $analysedQuery);
117
        }
118
119 36
        return $queryBuilder;
120
    }
121
122
    /**
123
     * @param QueryBuilder $queryBuilder
124
     * @param array|OrderingConfiguration[] $orderingConfigurations
125
     */
126 36
    private function applyOrdering(QueryBuilder $queryBuilder, array $orderingConfigurations)
127
    {
128 36
        foreach ($orderingConfigurations as $orderingConfiguration) {
129 36
            $queryBuilder->addOrderBy(
130 36
                $orderingConfiguration->getOrderByExpression(),
131 36
                $orderingConfiguration->isOrderAscending() ? 'ASC' : 'DESC'
132
            );
133
        }
134 36
    }
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 33
    private function applyAfter(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
147
    {
148 33
        $this->applyCursor($queryBuilder, $after, $analysedQuery, false);
149 33
    }
150
151
    /**
152
     * @param QueryBuilder $queryBuilder
153
     * @param string $after
154
     * @param AnalysedQuery $analysedQuery
155
     */
156 34
    private function applyBefore(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
157
    {
158 34
        $this->applyCursor($queryBuilder, $after, $analysedQuery, true);
159 34
    }
160
161
    /**
162
     * @param QueryBuilder $queryBuilder
163
     * @param string $cursor
164
     * @param AnalysedQuery $analysedQuery
165
     * @param bool $invert
166
     */
167 34
    private function applyCursor(QueryBuilder $queryBuilder, string $cursor, AnalysedQuery $analysedQuery, bool $invert)
168
    {
169 34
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
170 34
        $parsedCursor = $this->cursorBuilder->parseCursor($cursor, count($orderingConfigurations));
171
172 34
        $expr = new Expr();
173 34
        $whereClause = new Orx();
174 34
        $previousConditions = new Andx();
175 34
        foreach ($orderingConfigurations as $index => $orderingConfiguration) {
176 34
            $useLargerThan = $orderingConfiguration->isOrderAscending();
177 34
            if ($invert) {
178 34
                $useLargerThan = !$useLargerThan;
179
            }
180 34
            $sign = $useLargerThan ? '>' : '<';
181 34
            if ($parsedCursor->isCursoredItemIncluded() && $index === count($orderingConfigurations) - 1) {
182 11
                $sign .= '=';
183
            }
184
185 34
            $currentCondition = clone $previousConditions;
186 34
            $currentCondition->add(new Comparison(
187 34
                $orderingConfiguration->getOrderByExpression(),
188 34
                $sign,
189 34
                $expr->literal($parsedCursor->getElementAtIndex($index))
190
            ));
191
192 34
            $whereClause->add($currentCondition);
193
194 34
            $previousConditions->add(new Comparison(
195 34
                $orderingConfiguration->getOrderByExpression(),
196 34
                '=',
197 34
                $expr->literal($parsedCursor->getElementAtIndex($index))
198
            ));
199
        }
200
201 34
        $queryBuilder->andWhere($whereClause);
202 34
    }
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 36
    private function reverseOrderingDirection(array $orderingConfigurations): array
285
    {
286 36
        $reversedOrderingConfigurations = [];
287 36
        foreach ($orderingConfigurations as $orderingConfiguration) {
288 36
            $reversedOrderingConfigurations[] = (clone $orderingConfiguration)
289 36
                ->setOrderAscending(!$orderingConfiguration->isOrderAscending())
290
            ;
291
        }
292 36
        return $reversedOrderingConfigurations;
293
    }
294
295 32 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 32
        $nextPager = (new Pager())
298 32
            ->setBefore($previousCursor)
299 32
            ->setLimit(1)
300
        ;
301
302 32
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
303
    }
304
305 28 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 28
        $nextPager = (new Pager())
308 28
            ->setAfter($nextCursor)
309 28
            ->setLimit(1)
310
        ;
311
312 28
        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
339 3
        $query = $countQueryBuilder->getQuery();
340 3
        if ($analysedQuery->getQueryModifier() !== null) {
341
            $queryModifier = $analysedQuery->getQueryModifier();
342
            $query = $queryModifier($query);
343
        }
344
345 3
        return (int)$query->getSingleScalarResult();
346
    }
347
348 2
    private function findCountWithGroupBy(string $groupByColumn, AnalysedQuery $analysedQuery): int
349
    {
350 2
        $countQueryBuilder = $analysedQuery->cloneQueryBuilder();
351
        $countQueryBuilder
352 2
            ->resetDQLPart('groupBy')
353 2
            ->select(sprintf('count(distinct %s)', $groupByColumn))
354
        ;
355
356 2
        $nullQueryBuilder = $analysedQuery->cloneQueryBuilder()
357 2
            ->resetDQLPart('groupBy')
358 2
            ->select($analysedQuery->getRootAlias())
359 2
            ->setMaxResults(1)
360 2
            ->andWhere($groupByColumn . ' is null')
361
        ;
362
363 2
        $queryModifier = $analysedQuery->getQueryModifier();
364 2
        $nonNullCountQuery = $countQueryBuilder->getQuery();
365 2
        if ($queryModifier !== null) {
366
            $nonNullCountQuery = $queryModifier($nonNullCountQuery);
367
        }
368 2
        $nonNullCount = (int)$nonNullCountQuery->getSingleScalarResult();
369
370 2
        $nullQuery = $nullQueryBuilder->getQuery();
371 2
        if ($queryModifier !== null) {
372
            $nullQuery = $queryModifier($nullQuery);
373
        }
374 2
        $nullExists = count($nullQuery->getScalarResult());
375
376 2
        return $nonNullCount + $nullExists;
377
    }
378
379
    /**
380
     * @param QueryBuilder $queryBuilder
381
     * @return string|null
382
     */
383 7
    private function getSingleValidGroupByColumn(QueryBuilder $queryBuilder)
384
    {
385
        /** @var GroupBy[] $groupByParts */
386 7
        $groupByParts = $queryBuilder->getDQLPart('groupBy');
387
388 7
        if (count($groupByParts) === 0) {
389 3
            return null;
390
        }
391
392 4
        if (count($groupByParts) > 1) {
393 1
            $groupNames = array_map(
394 1
                function (GroupBy $groupBy) {
395 1
                    return $groupBy->getParts()[0];
396 1
                },
397 1
                $groupByParts
398
            );
399 1
            throw new InvalidGroupByException(implode(', ', $groupNames));
400
        }
401
402 3
        if (count($groupByParts) === 1 && count($groupByParts[0]->getParts()) > 1) {
403 1
            throw new InvalidGroupByException(implode(', ', $groupByParts[0]->getParts()));
404
        }
405
406 2
        return $groupByParts[0]->getParts()[0];
407
    }
408
409 1
    private function transformResultItems(callable $transform, Result $result)
410
    {
411 1
        $transformedItems = [];
412 1
        foreach ($result->getItems() as $item) {
413 1
            $transformedItems[] = $transform($item);
414
        }
415
416 1
        $result->setItems($transformedItems);
417 1
    }
418
}
419