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