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