Completed
Pull Request — master (#8)
by
unknown
03:30
created

ResultProvider   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 366
Duplicated Lines 4.37 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 16
dl 16
loc 366
ccs 201
cts 201
cp 1
rs 4.5599
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getTotalCountForQuery() 0 6 1
A buildResult() 0 19 2
A findItems() 0 11 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 30 6
A buildResultForZeroLimit() 0 20 2
A buildResultForTooLargeOffset() 0 19 2
A reverseOrderingDirection() 0 9 2
A existsBeforeCursor() 7 7 1
A existsAfterCursor() 7 7 1
A calculateTotalCount() 0 12 6
A findCount() 0 12 2
A findCountWithGroupBy() 0 18 1
A getSingleValidGroupByColumn() 0 25 5
A getResultForQuery() 0 20 4
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 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 35
    private function findItems(AnalysedQuery $analysedQuery, Pager $pager)
80
    {
81 35
        $pagedQueryBuilder = $this->pageQueryBuilder($analysedQuery, $pager);
82 35
        $query = $pagedQueryBuilder->getQuery();
83 35
        $items = $query->getResult();
84
85 35
        if ($pager->getBefore() !== null) {
86 33
            return array_reverse($items);
87
        }
88 35
        return $items;
89
    }
90
91 35
    private function pageQueryBuilder(AnalysedQuery $analysedQuery, Pager $pager)
92
    {
93 35
        $queryBuilder = $analysedQuery->cloneQueryBuilder();
94
95 35
        if ($pager->getLimit() !== null) {
96 35
            $queryBuilder->setMaxResults($pager->getLimit());
97
        }
98
99 35
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
100 35
        if ($pager->getBefore() !== null) {
101 33
            $orderingConfigurations = $this->reverseOrderingDirection($orderingConfigurations);
102
        }
103
104 35
        $this->applyOrdering($queryBuilder, $orderingConfigurations);
105
106 35
        if ($pager->getOffset() !== null) {
107 6
            $this->applyOffset($queryBuilder, $pager->getOffset());
108 35
        } elseif ($pager->getBefore() !== null) {
109 33
            $this->applyBefore($queryBuilder, $pager->getBefore(), $analysedQuery);
110 35
        } elseif ($pager->getAfter() !== null) {
111 32
            $this->applyAfter($queryBuilder, $pager->getAfter(), $analysedQuery);
112
        }
113
114 35
        return $queryBuilder;
115
    }
116
117
    /**
118
     * @param QueryBuilder $queryBuilder
119
     * @param array|OrderingConfiguration[] $orderingConfigurations
120
     */
121 35
    private function applyOrdering(QueryBuilder $queryBuilder, array $orderingConfigurations)
122
    {
123 35
        foreach ($orderingConfigurations as $orderingConfiguration) {
124 35
            $queryBuilder->addOrderBy(
125 35
                $orderingConfiguration->getOrderByExpression(),
126 35
                $orderingConfiguration->isOrderAscending() ? 'ASC' : 'DESC'
127
            );
128
        }
129 35
    }
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 32
    private function applyAfter(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
142
    {
143 32
        $this->applyCursor($queryBuilder, $after, $analysedQuery, false);
144 32
    }
145
146
    /**
147
     * @param QueryBuilder $queryBuilder
148
     * @param string $after
149
     * @param AnalysedQuery $analysedQuery
150
     */
151 33
    private function applyBefore(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
152
    {
153 33
        $this->applyCursor($queryBuilder, $after, $analysedQuery, true);
154 33
    }
155
156
    /**
157
     * @param QueryBuilder $queryBuilder
158
     * @param string $cursor
159
     * @param AnalysedQuery $analysedQuery
160
     * @param bool $invert
161
     */
162 33
    private function applyCursor(QueryBuilder $queryBuilder, string $cursor, AnalysedQuery $analysedQuery, bool $invert)
163
    {
164 33
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
165 33
        $parsedCursor = $this->cursorBuilder->parseCursor($cursor, count($orderingConfigurations));
166
167 33
        $expr = new Expr();
168 33
        $whereClause = new Orx();
169 33
        $previousConditions = new Andx();
170 33
        foreach ($orderingConfigurations as $index => $orderingConfiguration) {
171 33
            $useLargerThan = $orderingConfiguration->isOrderAscending();
172 33
            if ($invert) {
173 33
                $useLargerThan = !$useLargerThan;
174
            }
175 33
            $sign = $useLargerThan ? '>' : '<';
176 33
            if ($parsedCursor->isCursoredItemIncluded() && $index === count($orderingConfigurations) - 1) {
177 11
                $sign .= '=';
178
            }
179
180 33
            $currentCondition = clone $previousConditions;
181 33
            $currentCondition->add(new Comparison(
182 33
                $orderingConfiguration->getOrderByExpression(),
183 33
                $sign,
184 33
                $expr->literal($parsedCursor->getElementAtIndex($index))
185
            ));
186
187 33
            $whereClause->add($currentCondition);
188
189 33
            $previousConditions->add(new Comparison(
190 33
                $orderingConfiguration->getOrderByExpression(),
191 33
                '=',
192 33
                $expr->literal($parsedCursor->getElementAtIndex($index))
193
            ));
194
        }
195
196 33
        $queryBuilder->andWhere($whereClause);
197 33
    }
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 35
    private function reverseOrderingDirection(array $orderingConfigurations): array
276
    {
277 35
        $reversedOrderingConfigurations = [];
278 35
        foreach ($orderingConfigurations as $orderingConfiguration) {
279 35
            $reversedOrderingConfigurations[] = (clone $orderingConfiguration)
280 35
                ->setOrderAscending(!$orderingConfiguration->isOrderAscending());
281
        }
282 35
        return $reversedOrderingConfigurations;
283
    }
284
285 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...
286
    {
287 31
        $nextPager = (new Pager())
288 31
            ->setBefore($previousCursor)
289 31
            ->setLimit(1);
290 31
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
291
    }
292
293 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...
294
    {
295 27
        $nextPager = (new Pager())
296 27
            ->setAfter($nextCursor)
297 27
            ->setLimit(1);
298 27
        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 1
    private function transformResultItems(callable $transform, Result $result)
377
    {
378 1
        $transformedItems = [];
379 1
        foreach ($result->getItems() as $item) {
380 1
            $transformedItems[] = $transform($item);
381
        }
382
383 1
        $result->setItems($transformedItems);
384 1
    }
385
}
386