PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Database / Concerns / BuildsQueries.php

BuildsQueries.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at vendor/wpfluent/framework/src/WPFluent/Database/Concerns/BuildsQueries.php

568 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Database\Concerns;
4
5 use RuntimeException;
6 use InvalidArgumentException;
7 use FluentBoards\Framework\Container\Container;
8 use FluentBoards\Framework\Support\Str;
9 use FluentBoards\Framework\Support\Helper;
10 use FluentBoards\Framework\Support\Collection;
11 use FluentBoards\Framework\Support\Conditionable;
12 use FluentBoards\Framework\Support\LazyCollection;
13 use FluentBoards\Framework\Database\Orm\Builder;
14 use FluentBoards\Framework\Database\Query\Expression;
15 use FluentBoards\Framework\Database\RecordsNotFoundException;
16 use FluentBoards\Framework\Database\MultipleRecordsFoundException;
17 use FluentBoards\Framework\Pagination\Cursor;
18 use FluentBoards\Framework\Pagination\Paginator;
19 use FluentBoards\Framework\Pagination\CursorPaginator;
20 use FluentBoards\Framework\Pagination\LengthAwarePaginator;
21
22 /**
23 * @template TValue
24 *
25 * @mixin \FluentBoards\Framework\Database\Orm\Builder
26 * @mixin \FluentBoards\Framework\Database\Query\Builder
27 */
28 trait BuildsQueries
29 {
30 use Conditionable;
31
32 /**
33 * Chunk the results of the query.
34 *
35 * @param int $count
36 * @param callable(\FluentBoards\Framework\Support\Collection<int, TValue>, int): mixed $callback
37 * @return bool
38 */
39 public function chunk($count, callable $callback)
40 {
41 $this->enforceOrderBy();
42
43 $page = 1;
44
45 do {
46 // We'll execute the query for the given page and get the results. If there are
47 // no results we can just break and return from here. When there are results
48 // we will call the callback with the current chunk of these results here.
49 $results = $this->forPage($page, $count)->get();
50
51 $countResults = $results->count();
52
53 if ($countResults == 0) {
54 break;
55 }
56
57 // On each chunk result set, we will pass them to the callback and then let the
58 // developer take care of everything within the callback, which allows us to
59 // keep the memory low for spinning through large result sets for working.
60 if ($callback($results, $page) === false) {
61 return false;
62 }
63
64 unset($results);
65
66 $page++;
67 } while ($countResults == $count);
68
69 return true;
70 }
71
72 /**
73 * Run a map over each item while chunking.
74 *
75 * @template TReturn
76 *
77 * @param callable(TValue): TReturn $callback
78 * @param int $count
79 * @return \FluentBoards\Framework\Support\Collection<int, TReturn>
80 */
81 public function chunkMap(callable $callback, $count = 1000)
82 {
83 $collection = Collection::make();
84
85 $this->chunk($count, function ($items) use ($collection, $callback) {
86 $items->each(function ($item) use ($collection, $callback) {
87 $collection->push($callback($item));
88 });
89 });
90
91 return $collection;
92 }
93
94 /**
95 * Execute a callback over each item while chunking.
96 *
97 * @param callable(TValue, int): mixed $callback
98 * @param int $count
99 * @return bool
100 *
101 * @throws \RuntimeException
102 */
103 public function each(callable $callback, $count = 1000)
104 {
105 return $this->chunk($count, function ($results) use ($callback) {
106 foreach ($results as $key => $value) {
107 if ($callback($value, $key) === false) {
108 return false;
109 }
110 }
111 });
112 }
113
114 /**
115 * Chunk the results of a query by comparing IDs.
116 *
117 * @param int $count
118 * @param callable(\FluentBoards\Framework\Support\Collection<int, TValue>, int): mixed $callback
119 * @param string|null $column
120 * @param string|null $alias
121 * @return bool
122 */
123 public function chunkById($count, callable $callback, $column = null, $alias = null)
124 {
125 return $this->orderedChunkById($count, $callback, $column, $alias);
126 }
127
128 /**
129 * Chunk the results of a query by comparing IDs in descending order.
130 *
131 * @param int $count
132 * @param callable(\FluentBoards\Framework\Support\Collection<int, TValue>, int): mixed $callback
133 * @param string|null $column
134 * @param string|null $alias
135 * @return bool
136 */
137 public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
138 {
139 return $this->orderedChunkById(
140 $count, $callback, $column, $alias, true
141 );
142 }
143
144 /**
145 * Chunk the results of a query by comparing IDs in a given order.
146 *
147 * @param int $count
148 * @param callable(\FluentBoards\Framework\Support\Collection<int, TValue>, int): mixed $callback
149 * @param string|null $column
150 * @param string|null $alias
151 * @param bool $descending
152 * @return bool
153 *
154 * @throws \RuntimeException
155 */
156 public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false)
157 {
158 $column ??= $this->defaultKeyName();
159
160 $alias ??= $column;
161
162 $lastId = null;
163
164 $page = 1;
165
166 do {
167 $clone = clone $this;
168
169 // We'll execute the query for the given page and get the results. If there are
170 // no results we can just break and return from here. When there are results
171 // we will call the callback with the current chunk of these results here.
172 if ($descending) {
173 $results = $clone->forPageBeforeId($count, $lastId, $column)->get();
174 } else {
175 $results = $clone->forPageAfterId($count, $lastId, $column)->get();
176 }
177
178 $countResults = $results->count();
179
180 if ($countResults == 0) {
181 break;
182 }
183
184 // On each chunk result set, we will pass them to the callback and then let the
185 // developer take care of everything within the callback, which allows us to
186 // keep the memory low for spinning through large result sets for working.
187 if ($callback($results, $page) === false) {
188 return false;
189 }
190
191 $lastId = Helper::dataGet($results->last(), $alias);
192
193 if ($lastId === null) {
194 throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
195 }
196
197 unset($results);
198
199 $page++;
200 } while ($countResults == $count);
201
202 return true;
203 }
204
205 /**
206 * Execute a callback over each item while chunking by ID.
207 *
208 * @param callable(TValue, int): mixed $callback
209 * @param int $count
210 * @param string|null $column
211 * @param string|null $alias
212 * @return bool
213 */
214 public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
215 {
216 return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
217 foreach ($results as $key => $value) {
218 if ($callback($value, (($page - 1) * $count) + $key) === false) {
219 return false;
220 }
221 }
222 }, $column, $alias);
223 }
224
225 /**
226 * Query lazily, by chunks of the given size.
227 *
228 * @param int $chunkSize
229 * @return \FluentBoards\Framework\Support\LazyCollection
230 *
231 * @throws \InvalidArgumentException
232 */
233 public function lazy($chunkSize = 1000)
234 {
235 if ($chunkSize < 1) {
236 throw new InvalidArgumentException(
237 'The chunk size should be at least 1'
238 );
239 }
240
241 $this->enforceOrderBy();
242
243 return LazyCollection::make(function () use ($chunkSize) {
244 $page = 1;
245
246 while (true) {
247 $results = $this->forPage($page++, $chunkSize)->get();
248
249 foreach ($results as $result) {
250 yield $result;
251 }
252
253 if ($results->count() < $chunkSize) {
254 return;
255 }
256 }
257 });
258 }
259
260 /**
261 * Query lazily, by chunking the results of a query by comparing IDs.
262 *
263 * @param int $chunkSize
264 * @param string|null $column
265 * @param string|null $alias
266 * @return \FluentBoards\Framework\Support\LazyCollection
267 *
268 * @throws \InvalidArgumentException
269 */
270 public function lazyById($chunkSize = 1000, $column = null, $alias = null)
271 {
272 return $this->orderedLazyById($chunkSize, $column, $alias);
273 }
274
275 /**
276 * Query lazily, by chunking the results of a query by comparing IDs in descending order.
277 *
278 * @param int $chunkSize
279 * @param string|null $column
280 * @param string|null $alias
281 * @return \FluentBoards\Framework\Support\LazyCollection
282 *
283 * @throws \InvalidArgumentException
284 */
285 public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
286 {
287 return $this->orderedLazyById($chunkSize, $column, $alias, true);
288 }
289
290 /**
291 * Query lazily, by chunking the results of a query by comparing IDs in a given order.
292 *
293 * @param int $chunkSize
294 * @param string|null $column
295 * @param string|null $alias
296 * @param bool $descending
297 * @return \FluentBoards\Framework\Support\LazyCollection
298 *
299 * @throws \InvalidArgumentException
300 */
301 protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
302 {
303 if ($chunkSize < 1) {
304 throw new InvalidArgumentException(
305 'The chunk size should be at least 1'
306 );
307 }
308
309 $column ??= $this->defaultKeyName();
310
311 $alias ??= $column;
312
313 return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) {
314 $lastId = null;
315
316 while (true) {
317 $clone = clone $this;
318
319 if ($descending) {
320 $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
321 } else {
322 $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
323 }
324
325 foreach ($results as $result) {
326 yield $result;
327 }
328
329 if ($results->count() < $chunkSize) {
330 return;
331 }
332
333 $lastId = $results->last()->{$alias};
334
335 if ($lastId === null) {
336 throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
337 }
338 }
339 });
340 }
341
342 /**
343 * Execute the query and get the first result.
344 *
345 * @param array|string $columns
346 * @return TValue|null
347 */
348 public function first($columns = ['*'])
349 {
350 return $this->take(1)->get($columns)->first();
351 }
352
353 /**
354 * Execute the query and get the first result if it's the sole matching record.
355 *
356 * @param array|string $columns
357 * @return TValue
358 *
359 * @throws \FluentBoards\Framework\Database\RecordsNotFoundException
360 * @throws \FluentBoards\Framework\Database\MultipleRecordsFoundException
361 */
362 public function sole($columns = ['*'])
363 {
364 $result = $this->take(2)->get($columns);
365
366 $count = $result->count();
367
368 if ($count === 0) {
369 throw new RecordsNotFoundException;
370 }
371
372 if ($count > 1) {
373 throw new MultipleRecordsFoundException($count);
374 }
375
376 return $result->first();
377 }
378
379 /**
380 * Paginate the given query using a cursor paginator.
381 *
382 * @param int $perPage
383 * @param array|string $columns
384 * @param string $cursorName
385 * @param \FluentBoards\Framework\Pagination\Cursor|string|null $cursor
386 * @return \FluentBoards\Framework\Pagination\CursorPaginator
387 */
388 protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
389 {
390 if (! $cursor instanceof Cursor) {
391 $cursor = is_string($cursor)
392 ? Cursor::fromEncoded($cursor)
393 : CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
394 }
395
396 $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
397
398 if (! is_null($cursor)) {
399 // Reset the union bindings so we can add the cursor where in the correct position...
400 $this->setBindings([], 'union');
401
402 $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
403 $unionBuilders = $builder->getUnionBuilders();
404
405 if (! is_null($previousColumn)) {
406 $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
407
408 $builder->where(
409 Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
410 '=',
411 $cursor->parameter($previousColumn)
412 );
413
414 $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
415 $unionBuilder->where(
416 $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
417 '=',
418 $cursor->parameter($previousColumn)
419 );
420
421 $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
422 });
423 }
424
425 $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
426 ['column' => $column, 'direction' => $direction] = $orders[$i];
427
428 $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
429
430 $secondBuilder->where(
431 Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
432 $direction === 'asc' ? '>' : '<',
433 $cursor->parameter($column)
434 );
435
436 if ($i < $orders->count() - 1) {
437 $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
438 $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
439 });
440 }
441
442 $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
443 $unionWheres = $unionBuilder->getRawBindings()['where'];
444
445 $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
446 $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
447 $unionBuilder->where(
448 $originalColumn,
449 $direction === 'asc' ? '>' : '<',
450 $cursor->parameter($column)
451 );
452
453 if ($i < $orders->count() - 1) {
454 $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
455 $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
456 });
457 }
458
459 $this->addBinding($unionWheres, 'union');
460 $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
461 });
462 });
463 });
464 };
465
466 $addCursorConditions($this, null, null, 0);
467 }
468
469 $this->limit($perPage + 1);
470
471 return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
472 'path' => Paginator::resolveCurrentPath(),
473 'cursorName' => $cursorName,
474 'parameters' => $orders->pluck('column')->toArray(),
475 ]);
476 }
477
478 /**
479 * Get the original column name of the given column, without any aliasing.
480 *
481 * @param \FluentBoards\Framework\Database\Query\Builder|\FluentBoards\Framework\Database\Orm\Builder<*> $builder
482 * @param string $parameter
483 * @return string
484 */
485 protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
486 {
487 $columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
488
489 if (! is_null($columns)) {
490 foreach ($columns as $column) {
491 if (($position = strripos($column, ' as ')) !== false) {
492 $original = substr($column, 0, $position);
493
494 $alias = substr($column, $position + 4);
495
496 if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
497 return $original;
498 }
499 }
500 }
501 }
502
503 return $parameter;
504 }
505
506 /**
507 * Create a new length-aware paginator instance.
508 *
509 * @param \FluentBoards\Framework\Support\Collection $items
510 * @param int $total
511 * @param int $perPage
512 * @param int $currentPage
513 * @param array $options
514 * @return \FluentBoards\Framework\Pagination\LengthAwarePaginator
515 */
516 protected function paginator($items, $total, $perPage, $currentPage, $options)
517 {
518 return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
519 'items', 'total', 'perPage', 'currentPage', 'options'
520 ));
521 }
522
523 /**
524 * Create a new simple paginator instance.
525 *
526 * @param \FluentBoards\Framework\Support\Collection $items
527 * @param int $perPage
528 * @param int $currentPage
529 * @param array $options
530 * @return \FluentBoards\Framework\Pagination\Paginator
531 */
532 protected function simplePaginator($items, $perPage, $currentPage, $options)
533 {
534 return Container::getInstance()->makeWith(Paginator::class, compact(
535 'items', 'perPage', 'currentPage', 'options'
536 ));
537 }
538
539 /**
540 * Create a new cursor paginator instance.
541 *
542 * @param \FluentBoards\Framework\Support\Collection $items
543 * @param int $perPage
544 * @param \FluentBoards\Framework\Pagination\Cursor $cursor
545 * @param array $options
546 * @return \FluentBoards\Framework\Pagination\CursorPaginator
547 */
548 protected function cursorPaginator($items, $perPage, $cursor, $options)
549 {
550 return Container::getInstance()->makeWith(CursorPaginator::class, compact(
551 'items', 'perPage', 'cursor', 'options'
552 ));
553 }
554
555 /**
556 * Pass the query to a given callback.
557 *
558 * @param callable($this): mixed $callback
559 * @return $this
560 */
561 public function tap($callback)
562 {
563 $callback($this);
564
565 return $this;
566 }
567 }
568