Completed
Pull Request — master (#2)
by Valentas
02:12
created

ResultProvider   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 330
Duplicated Lines 4.85 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 16
dl 16
loc 330
ccs 179
cts 179
cp 1
rs 6.96
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getResultForQuery() 0 16 3
A getTotalCountForQuery() 0 6 1
A buildResult() 0 20 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 33 6
A buildResultForZeroLimit() 0 21 2
A buildResultForTooLargeOffset() 0 20 2
A reverseOrderingDirection() 0 10 2
A existsBeforeCursor() 8 8 1
A existsAfterCursor() 8 8 1
A calculateTotalCount() 0 12 6
A findCount() 0 13 2
A validateGroupByParts() 0 16 4

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\Comparison;
10
use Doctrine\ORM\QueryBuilder;
11
use Paysera\Pagination\Entity\Doctrine\AnalysedQuery;
12
use Paysera\Pagination\Entity\OrderingConfiguration;
13
use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery;
14
use Paysera\Pagination\Entity\Pager;
15
use Paysera\Pagination\Entity\Result;
16
use Paysera\Pagination\Exception\InvalidGroupByException;
17
use Paysera\Pagination\Service\CursorBuilderInterface;
18
19
class ResultProvider
20
{
21
    private $queryAnalyser;
22
    private $cursorBuilder;
23
24 36
    public function __construct(QueryAnalyser $queryAnalyser, CursorBuilderInterface $cursorBuilder)
25
    {
26 36
        $this->queryAnalyser = $queryAnalyser;
27 36
        $this->cursorBuilder = $cursorBuilder;
28 36
    }
29
30 31
    public function getResultForQuery(ConfiguredQuery $configuredQuery, Pager $pager): Result
31
    {
32 31
        $analysedQuery = $this->queryAnalyser->analyseQuery($configuredQuery, $pager);
33
34 30
        $result = $this->buildResult($analysedQuery, $pager);
35
36 30
        if ($configuredQuery->isTotalCountNeeded()) {
37 2
            $totalCount = $this->calculateTotalCount($pager, count($result->getItems()));
38 2
            if ($totalCount === null) {
39 1
                $totalCount = $this->findCount($analysedQuery);
40
            }
41 2
            $result->setTotalCount($totalCount);
42
        }
43
44 30
        return $result;
45
    }
46
47 5
    public function getTotalCountForQuery(ConfiguredQuery $configuredQuery): int
48
    {
49 5
        $analysedQuery = $this->queryAnalyser->analyseQueryWithoutPager($configuredQuery);
50
51 5
        return $this->findCount($analysedQuery);
52
    }
53
54 30
    private function buildResult(AnalysedQuery $analysedQuery, Pager $pager)
55
    {
56 30
        $items = $this->findItems($analysedQuery, $pager);
57
58 30
        if (count($items) === 0) {
59 11
            return $this->buildResultForEmptyItems($analysedQuery, $pager);
60
        }
61
62 20
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
63 20
        $previousCursor = $this->cursorBuilder->getCursorFromItem($items[0], $orderingConfigurations);
64 20
        $nextCursor = $this->cursorBuilder->getCursorFromItem($items[count($items) - 1], $orderingConfigurations);
65
66 20
        return (new Result())
67 20
            ->setItems($items)
68 20
            ->setPreviousCursor($previousCursor)
69 20
            ->setNextCursor($nextCursor)
70 20
            ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
71 20
            ->setHasNext($this->existsAfterCursor($nextCursor, $analysedQuery))
72
        ;
73
    }
74
75 30
    private function findItems(AnalysedQuery $analysedQuery, Pager $pager)
76
    {
77 30
        $pagedQueryBuilder = $this->pageQueryBuilder($analysedQuery, $pager);
78 30
        $query = $pagedQueryBuilder->getQuery();
79 30
        $items = $query->getResult();
80
81 30
        if ($pager->getBefore() !== null) {
82 28
            return array_reverse($items);
83
        }
84 30
        return $items;
85
    }
86
87 30
    private function pageQueryBuilder(AnalysedQuery $analysedQuery, Pager $pager)
88
    {
89 30
        $queryBuilder = $analysedQuery->cloneQueryBuilder();
90
91 30
        if ($pager->getLimit() !== null) {
92 30
            $queryBuilder->setMaxResults($pager->getLimit());
93
        }
94
95 30
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
96 30
        if ($pager->getBefore() !== null) {
97 28
            $orderingConfigurations = $this->reverseOrderingDirection($orderingConfigurations);
98
        }
99
100 30
        $this->applyOrdering($queryBuilder, $orderingConfigurations);
101
102 30
        if ($pager->getOffset() !== null) {
103 6
            $this->applyOffset($queryBuilder, $pager->getOffset());
104 30
        } elseif ($pager->getBefore() !== null) {
105 28
            $this->applyBefore($queryBuilder, $pager->getBefore(), $analysedQuery);
106 30
        } elseif ($pager->getAfter() !== null) {
107 27
            $this->applyAfter($queryBuilder, $pager->getAfter(), $analysedQuery);
108
        }
109
110 30
        return $queryBuilder;
111
    }
112
113
    /**
114
     * @param QueryBuilder $queryBuilder
115
     * @param array|OrderingConfiguration[] $orderingConfigurations
116
     */
117 30
    private function applyOrdering(QueryBuilder $queryBuilder, array $orderingConfigurations)
118
    {
119 30
        foreach ($orderingConfigurations as $orderingConfiguration) {
120 30
            $queryBuilder->addOrderBy(
121 30
                $orderingConfiguration->getOrderByExpression(),
122 30
                $orderingConfiguration->isOrderAscending() ? 'ASC' : 'DESC'
123
            );
124
        }
125 30
    }
126
127 6
    private function applyOffset(QueryBuilder $queryBuilder, int $offset)
128
    {
129 6
        $queryBuilder->setFirstResult($offset);
130 6
    }
131
132
    /**
133
     * @param QueryBuilder $queryBuilder
134
     * @param string $after
135
     * @param AnalysedQuery $analysedQuery
136
     */
137 27
    private function applyAfter(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
138
    {
139 27
        $this->applyCursor($queryBuilder, $after, $analysedQuery, false);
140 27
    }
141
142
    /**
143
     * @param QueryBuilder $queryBuilder
144
     * @param string $after
145
     * @param AnalysedQuery $analysedQuery
146
     */
147 28
    private function applyBefore(QueryBuilder $queryBuilder, string $after, AnalysedQuery $analysedQuery)
148
    {
149 28
        $this->applyCursor($queryBuilder, $after, $analysedQuery, true);
150 28
    }
151
152
    /**
153
     * @param QueryBuilder $queryBuilder
154
     * @param string $cursor
155
     * @param AnalysedQuery $analysedQuery
156
     * @param bool $invert
157
     */
158 28
    private function applyCursor(QueryBuilder $queryBuilder, string $cursor, AnalysedQuery $analysedQuery, bool $invert)
159
    {
160 28
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
161 28
        $parsedCursor = $this->cursorBuilder->parseCursor($cursor, count($orderingConfigurations));
162
163 28
        $expr = new Expr();
164 28
        $whereClause = new Orx();
165 28
        $previousConditions = new Andx();
166 28
        foreach ($orderingConfigurations as $index => $orderingConfiguration) {
167 28
            $useLargerThan = $orderingConfiguration->isOrderAscending();
168 28
            if ($invert) {
169 28
                $useLargerThan = !$useLargerThan;
170
            }
171 28
            $sign = $useLargerThan ? '>' : '<';
172 28
            if ($parsedCursor->isCursoredItemIncluded() && $index === count($orderingConfigurations) - 1) {
173 11
                $sign .= '=';
174
            }
175
176 28
            $currentCondition = clone $previousConditions;
177 28
            $currentCondition->add(new Comparison(
178 28
                $orderingConfiguration->getOrderByExpression(),
179 28
                $sign,
180 28
                $expr->literal($parsedCursor->getElementAtIndex($index))
181
            ));
182
183 28
            $whereClause->add($currentCondition);
184
185 28
            $previousConditions->add(new Comparison(
186 28
                $orderingConfiguration->getOrderByExpression(),
187 28
                '=',
188 28
                $expr->literal($parsedCursor->getElementAtIndex($index))
189
            ));
190
        }
191
192 28
        $queryBuilder->andWhere($whereClause);
193 28
    }
194
195 11
    private function buildResultForEmptyItems(AnalysedQuery $analysedQuery, Pager $pager): Result
196
    {
197 11
        if ($pager->getLimit() === 0) {
198 3
            return $this->buildResultForZeroLimit($analysedQuery, $pager);
199
200 9
        } elseif ($pager->getBefore() !== null) {
201 2
            $nextCursor = $this->cursorBuilder->invertCursorInclusion($pager->getBefore());
202 2
            return (new Result())
203 2
                ->setPreviousCursor($pager->getBefore())
204 2
                ->setNextCursor($nextCursor)
205 2
                ->setHasPrevious(false)
206 2
                ->setHasNext($this->existsAfterCursor($nextCursor, $analysedQuery))
207
            ;
208
209 7
        } elseif ($pager->getAfter() !== null) {
210 5
            $previousCursor = $this->cursorBuilder->invertCursorInclusion($pager->getAfter());
211 5
            return (new Result())
212 5
                ->setPreviousCursor($previousCursor)
213 5
                ->setNextCursor($pager->getAfter())
214 5
                ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
215 5
                ->setHasNext(false)
216
            ;
217
218 3
        } elseif ($pager->getOffset() !== null && $pager->getOffset() > 0) {
219 3
            return $this->buildResultForTooLargeOffset($analysedQuery);
220
221
        }
222
223 1
        return (new Result())
224 1
            ->setHasPrevious(false)
225 1
            ->setHasNext(false)
226
        ;
227
    }
228
229 3
    private function buildResultForZeroLimit(AnalysedQuery $analysedQuery, Pager $zeroLimitPager): Result
230
    {
231 3
        $pager = (clone $zeroLimitPager)->setLimit(1);
232 3
        $items = $this->findItems($analysedQuery, $pager);
233
234 3
        if (count($items) === 0) {
235 1
            return $this->buildResultForEmptyItems($analysedQuery, $pager);
236
        }
237
238 2
        $orderingConfigurations = $analysedQuery->getOrderingConfigurations();
239 2
        $previousCursor = $this->cursorBuilder->getCursorFromItem($items[0], $orderingConfigurations);
240 2
        $nextCursor = $this->cursorBuilder->buildCursorWithIncludedItem($previousCursor);
241
242 2
        return (new Result())
243 2
            ->setPreviousCursor($previousCursor)
244 2
            ->setNextCursor($nextCursor)
245 2
            ->setHasPrevious($this->existsBeforeCursor($previousCursor, $analysedQuery))
246 2
            ->setHasNext(true)
247
        ;
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
    /**
273
     * @param array|OrderingConfiguration[] $orderingConfigurations
274
     * @return array|OrderingConfiguration[]
275
     */
276 30
    private function reverseOrderingDirection(array $orderingConfigurations): array
277
    {
278 30
        $reversedOrderingConfigurations = [];
279 30
        foreach ($orderingConfigurations as $orderingConfiguration) {
280 30
            $reversedOrderingConfigurations[] = (clone $orderingConfiguration)
281 30
                ->setOrderAscending(!$orderingConfiguration->isOrderAscending())
282
            ;
283
        }
284 30
        return $reversedOrderingConfigurations;
285
    }
286
287 26 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...
288
    {
289 26
        $nextPager = (new Pager())
290 26
            ->setBefore($previousCursor)
291 26
            ->setLimit(1)
292
        ;
293 26
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
294
    }
295
296 22 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...
297
    {
298 22
        $nextPager = (new Pager())
299 22
            ->setAfter($nextCursor)
300 22
            ->setLimit(1)
301
        ;
302 22
        return count($this->findItems($analysedQuery, $nextPager)) > 0;
303
    }
304
305 2
    private function calculateTotalCount(Pager $filter, int $resultCount)
306
    {
307
        if (
308 2
            $filter->getOffset() !== null
309 2
            && ($filter->getLimit() === null || $resultCount < $filter->getLimit())
310 2
            && ($resultCount !== 0 || $filter->getOffset() === 0)
311
        ) {
312 1
            return $resultCount + $filter->getOffset();
313
        }
314
315 1
        return null;
316
    }
317
318 6
    private function findCount(AnalysedQuery $analysedQuery): int
319
    {
320 6
        $countQueryBuilder = $analysedQuery->cloneQueryBuilder();
321 6
        $groupByParts = $countQueryBuilder->getDQLPart('groupBy');
322 6
        $countQueryBuilder->select(sprintf('count(%s)', $analysedQuery->getRootAlias()));
323
324 6
        $this->validateGroupByParts($groupByParts);
325 4
        if (count($groupByParts) === 1) {
326 1
            return count($countQueryBuilder->getQuery()->getArrayResult());
327
        }
328
329 3
        return (int)$countQueryBuilder->getQuery()->getSingleScalarResult();
330
    }
331
332 6
    private function validateGroupByParts(array $groupByParts)
333
    {
334 6
        if (count($groupByParts) > 1) {
335 1
            $groupNames = array_map(
336 1
                function (Expr\GroupBy $groupBy) {
337 1
                    return $groupBy->getParts()[0];
338 1
                },
339 1
                $groupByParts
340
            );
341 1
            throw new InvalidGroupByException(implode(', ', $groupNames));
342
        }
343
344 5
        if (count($groupByParts) === 1 && count($groupByParts[0]->getParts()) > 1) {
345 1
            throw new InvalidGroupByException(implode(', ', $groupByParts[0]->getParts()));
346
        }
347 4
    }
348
}
349