PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 1.0.97
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v1.0.97
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / vendor / wpfluent / framework / src / WPFluent / Database / Concerns / BuildsQueries.php

BuildsQueries.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 1.0.97, at vendor/wpfluent/framework/src/WPFluent/Database/Concerns/BuildsQueries.php

564 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Framework\Database\Concerns;
4
5 use RuntimeException;
6 use InvalidArgumentException;
7 use FluentCommunity\Framework\Container\Container;
8 use FluentCommunity\Framework\Support\Str;
9 use FluentCommunity\Framework\Support\Helper;
10 use FluentCommunity\Framework\Support\Collection;
11 use FluentCommunity\Framework\Support\Conditionable;
12 use FluentCommunity\Framework\Support\LazyCollection;
13 use FluentCommunity\Framework\Database\Orm\Builder;
14 use FluentCommunity\Framework\Database\Query\Expression;
15 use FluentCommunity\Framework\Database\RecordsNotFoundException;
16 use FluentCommunity\Framework\Database\MultipleRecordsFoundException;
17 use FluentCommunity\Framework\Pagination\Cursor;
18 use FluentCommunity\Framework\Pagination\Paginator;
19 use FluentCommunity\Framework\Pagination\CursorPaginator;
20 use FluentCommunity\Framework\Pagination\LengthAwarePaginator;
21
22 /**
23 * @template TValue
24 *
25 * @mixin \FluentCommunity\Framework\Database\Orm\Builder
26 * @mixin \FluentCommunity\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(\FluentCommunity\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 \FluentCommunity\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(\FluentCommunity\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(\FluentCommunity\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(\FluentCommunity\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 = data_get($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 \FluentCommunity\Framework\Support\LazyCollection
230 *
231 * @throws \InvalidArgumentException
232 */
233 public function lazy($chunkSize = 1000)
234 {
235 if ($chunkSize < 1) {
236 throw new InvalidArgumentException('The chunk size should be at least 1');
237 }
238
239 $this->enforceOrderBy();
240
241 return LazyCollection::make(function () use ($chunkSize) {
242 $page = 1;
243
244 while (true) {
245 $results = $this->forPage($page++, $chunkSize)->get();
246
247 foreach ($results as $result) {
248 yield $result;
249 }
250
251 if ($results->count() < $chunkSize) {
252 return;
253 }
254 }
255 });
256 }
257
258 /**
259 * Query lazily, by chunking the results of a query by comparing IDs.
260 *
261 * @param int $chunkSize
262 * @param string|null $column
263 * @param string|null $alias
264 * @return \FluentCommunity\Framework\Support\LazyCollection
265 *
266 * @throws \InvalidArgumentException
267 */
268 public function lazyById($chunkSize = 1000, $column = null, $alias = null)
269 {
270 return $this->orderedLazyById($chunkSize, $column, $alias);
271 }
272
273 /**
274 * Query lazily, by chunking the results of a query by comparing IDs in descending order.
275 *
276 * @param int $chunkSize
277 * @param string|null $column
278 * @param string|null $alias
279 * @return \FluentCommunity\Framework\Support\LazyCollection
280 *
281 * @throws \InvalidArgumentException
282 */
283 public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
284 {
285 return $this->orderedLazyById($chunkSize, $column, $alias, true);
286 }
287
288 /**
289 * Query lazily, by chunking the results of a query by comparing IDs in a given order.
290 *
291 * @param int $chunkSize
292 * @param string|null $column
293 * @param string|null $alias
294 * @param bool $descending
295 * @return \FluentCommunity\Framework\Support\LazyCollection
296 *
297 * @throws \InvalidArgumentException
298 */
299 protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
300 {
301 if ($chunkSize < 1) {
302 throw new InvalidArgumentException('The chunk size should be at least 1');
303 }
304
305 $column ??= $this->defaultKeyName();
306
307 $alias ??= $column;
308
309 return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) {
310 $lastId = null;
311
312 while (true) {
313 $clone = clone $this;
314
315 if ($descending) {
316 $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
317 } else {
318 $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
319 }
320
321 foreach ($results as $result) {
322 yield $result;
323 }
324
325 if ($results->count() < $chunkSize) {
326 return;
327 }
328
329 $lastId = $results->last()->{$alias};
330
331 if ($lastId === null) {
332 throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
333 }
334 }
335 });
336 }
337
338 /**
339 * Execute the query and get the first result.
340 *
341 * @param array|string $columns
342 * @return TValue|null
343 */
344 public function first($columns = ['*'])
345 {
346 return $this->take(1)->get($columns)->first();
347 }
348
349 /**
350 * Execute the query and get the first result if it's the sole matching record.
351 *
352 * @param array|string $columns
353 * @return TValue
354 *
355 * @throws \FluentCommunity\Framework\Database\RecordsNotFoundException
356 * @throws \FluentCommunity\Framework\Database\MultipleRecordsFoundException
357 */
358 public function sole($columns = ['*'])
359 {
360 $result = $this->take(2)->get($columns);
361
362 $count = $result->count();
363
364 if ($count === 0) {
365 throw new RecordsNotFoundException;
366 }
367
368 if ($count > 1) {
369 throw new MultipleRecordsFoundException($count);
370 }
371
372 return $result->first();
373 }
374
375 /**
376 * Paginate the given query using a cursor paginator.
377 *
378 * @param int $perPage
379 * @param array|string $columns
380 * @param string $cursorName
381 * @param \FluentCommunity\Framework\Pagination\Cursor|string|null $cursor
382 * @return \FluentCommunity\Framework\Contracts\Pagination\CursorPaginator
383 */
384 protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
385 {
386 if (! $cursor instanceof Cursor) {
387 $cursor = is_string($cursor)
388 ? Cursor::fromEncoded($cursor)
389 : CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
390 }
391
392 $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
393
394 if (! is_null($cursor)) {
395 // Reset the union bindings so we can add the cursor where in the correct position...
396 $this->setBindings([], 'union');
397
398 $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
399 $unionBuilders = $builder->getUnionBuilders();
400
401 if (! is_null($previousColumn)) {
402 $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
403
404 $builder->where(
405 Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
406 '=',
407 $cursor->parameter($previousColumn)
408 );
409
410 $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
411 $unionBuilder->where(
412 $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
413 '=',
414 $cursor->parameter($previousColumn)
415 );
416
417 $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
418 });
419 }
420
421 $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
422 ['column' => $column, 'direction' => $direction] = $orders[$i];
423
424 $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
425
426 $secondBuilder->where(
427 Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
428 $direction === 'asc' ? '>' : '<',
429 $cursor->parameter($column)
430 );
431
432 if ($i < $orders->count() - 1) {
433 $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
434 $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
435 });
436 }
437
438 $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
439 $unionWheres = $unionBuilder->getRawBindings()['where'];
440
441 $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
442 $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
443 $unionBuilder->where(
444 $originalColumn,
445 $direction === 'asc' ? '>' : '<',
446 $cursor->parameter($column)
447 );
448
449 if ($i < $orders->count() - 1) {
450 $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
451 $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
452 });
453 }
454
455 $this->addBinding($unionWheres, 'union');
456 $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
457 });
458 });
459 });
460 };
461
462 $addCursorConditions($this, null, null, 0);
463 }
464
465 $this->limit($perPage + 1);
466
467 return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
468 'path' => Paginator::resolveCurrentPath(),
469 'cursorName' => $cursorName,
470 'parameters' => $orders->pluck('column')->toArray(),
471 ]);
472 }
473
474 /**
475 * Get the original column name of the given column, without any aliasing.
476 *
477 * @param \FluentCommunity\Framework\Database\Query\Builder|\FluentCommunity\Framework\Database\Orm\Builder<*> $builder
478 * @param string $parameter
479 * @return string
480 */
481 protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
482 {
483 $columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
484
485 if (! is_null($columns)) {
486 foreach ($columns as $column) {
487 if (($position = strripos($column, ' as ')) !== false) {
488 $original = substr($column, 0, $position);
489
490 $alias = substr($column, $position + 4);
491
492 if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
493 return $original;
494 }
495 }
496 }
497 }
498
499 return $parameter;
500 }
501
502 /**
503 * Create a new length-aware paginator instance.
504 *
505 * @param \FluentCommunity\Framework\Support\Collection $items
506 * @param int $total
507 * @param int $perPage
508 * @param int $currentPage
509 * @param array $options
510 * @return \FluentCommunity\Framework\Pagination\LengthAwarePaginator
511 */
512 protected function paginator($items, $total, $perPage, $currentPage, $options)
513 {
514 return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
515 'items', 'total', 'perPage', 'currentPage', 'options'
516 ));
517 }
518
519 /**
520 * Create a new simple paginator instance.
521 *
522 * @param \FluentCommunity\Framework\Support\Collection $items
523 * @param int $perPage
524 * @param int $currentPage
525 * @param array $options
526 * @return \FluentCommunity\Framework\Pagination\Paginator
527 */
528 protected function simplePaginator($items, $perPage, $currentPage, $options)
529 {
530 return Container::getInstance()->makeWith(Paginator::class, compact(
531 'items', 'perPage', 'currentPage', 'options'
532 ));
533 }
534
535 /**
536 * Create a new cursor paginator instance.
537 *
538 * @param \FluentCommunity\Framework\Support\Collection $items
539 * @param int $perPage
540 * @param \FluentCommunity\Framework\Pagination\Cursor $cursor
541 * @param array $options
542 * @return \FluentCommunity\Framework\Pagination\CursorPaginator
543 */
544 protected function cursorPaginator($items, $perPage, $cursor, $options)
545 {
546 return Container::getInstance()->makeWith(CursorPaginator::class, compact(
547 'items', 'perPage', 'cursor', 'options'
548 ));
549 }
550
551 /**
552 * Pass the query to a given callback.
553 *
554 * @param callable($this): mixed $callback
555 * @return $this
556 */
557 public function tap($callback)
558 {
559 $callback($this);
560
561 return $this;
562 }
563 }
564