Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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