| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- <?php
- namespace Illuminate\Database\Concerns;
- use Illuminate\Container\Container;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Database\MultipleRecordsFoundException;
- use Illuminate\Database\Query\Expression;
- use Illuminate\Database\RecordsNotFoundException;
- use Illuminate\Pagination\Cursor;
- use Illuminate\Pagination\CursorPaginator;
- use Illuminate\Pagination\LengthAwarePaginator;
- use Illuminate\Pagination\Paginator;
- use Illuminate\Support\Collection;
- use Illuminate\Support\LazyCollection;
- use Illuminate\Support\Str;
- use Illuminate\Support\Traits\Conditionable;
- use InvalidArgumentException;
- use RuntimeException;
- trait BuildsQueries
- {
- use Conditionable;
- /**
- * Chunk the results of the query.
- *
- * @param int $count
- * @param callable $callback
- * @return bool
- */
- public function chunk($count, callable $callback)
- {
- $this->enforceOrderBy();
- $page = 1;
- do {
- // We'll execute the query for the given page and get the results. If there are
- // no results we can just break and return from here. When there are results
- // we will call the callback with the current chunk of these results here.
- $results = $this->forPage($page, $count)->get();
- $countResults = $results->count();
- if ($countResults == 0) {
- break;
- }
- // On each chunk result set, we will pass them to the callback and then let the
- // developer take care of everything within the callback, which allows us to
- // keep the memory low for spinning through large result sets for working.
- if ($callback($results, $page) === false) {
- return false;
- }
- unset($results);
- $page++;
- } while ($countResults == $count);
- return true;
- }
- /**
- * Run a map over each item while chunking.
- *
- * @param callable $callback
- * @param int $count
- * @return \Illuminate\Support\Collection
- */
- public function chunkMap(callable $callback, $count = 1000)
- {
- $collection = Collection::make();
- $this->chunk($count, function ($items) use ($collection, $callback) {
- $items->each(function ($item) use ($collection, $callback) {
- $collection->push($callback($item));
- });
- });
- return $collection;
- }
- /**
- * Execute a callback over each item while chunking.
- *
- * @param callable $callback
- * @param int $count
- * @return bool
- *
- * @throws \RuntimeException
- */
- public function each(callable $callback, $count = 1000)
- {
- return $this->chunk($count, function ($results) use ($callback) {
- foreach ($results as $key => $value) {
- if ($callback($value, $key) === false) {
- return false;
- }
- }
- });
- }
- /**
- * Chunk the results of a query by comparing IDs.
- *
- * @param int $count
- * @param callable $callback
- * @param string|null $column
- * @param string|null $alias
- * @return bool
- */
- public function chunkById($count, callable $callback, $column = null, $alias = null)
- {
- return $this->orderedChunkById($count, $callback, $column, $alias);
- }
- /**
- * Chunk the results of a query by comparing IDs in descending order.
- *
- * @param int $count
- * @param callable $callback
- * @param string|null $column
- * @param string|null $alias
- * @return bool
- */
- public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
- {
- return $this->orderedChunkById($count, $callback, $column, $alias, descending: true);
- }
- /**
- * Chunk the results of a query by comparing IDs in a given order.
- *
- * @param int $count
- * @param callable $callback
- * @param string|null $column
- * @param string|null $alias
- * @param bool $descending
- * @return bool
- *
- * @throws \RuntimeException
- */
- public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false)
- {
- $column ??= $this->defaultKeyName();
- $alias ??= $column;
- $lastId = null;
- $page = 1;
- do {
- $clone = clone $this;
- // We'll execute the query for the given page and get the results. If there are
- // no results we can just break and return from here. When there are results
- // we will call the callback with the current chunk of these results here.
- if ($descending) {
- $results = $clone->forPageBeforeId($count, $lastId, $column)->get();
- } else {
- $results = $clone->forPageAfterId($count, $lastId, $column)->get();
- }
- $countResults = $results->count();
- if ($countResults == 0) {
- break;
- }
- // On each chunk result set, we will pass them to the callback and then let the
- // developer take care of everything within the callback, which allows us to
- // keep the memory low for spinning through large result sets for working.
- if ($callback($results, $page) === false) {
- return false;
- }
- $lastId = data_get($results->last(), $alias);
- if ($lastId === null) {
- throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
- }
- unset($results);
- $page++;
- } while ($countResults == $count);
- return true;
- }
- /**
- * Execute a callback over each item while chunking by ID.
- *
- * @param callable $callback
- * @param int $count
- * @param string|null $column
- * @param string|null $alias
- * @return bool
- */
- public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
- {
- return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
- foreach ($results as $key => $value) {
- if ($callback($value, (($page - 1) * $count) + $key) === false) {
- return false;
- }
- }
- }, $column, $alias);
- }
- /**
- * Query lazily, by chunks of the given size.
- *
- * @param int $chunkSize
- * @return \Illuminate\Support\LazyCollection
- *
- * @throws \InvalidArgumentException
- */
- public function lazy($chunkSize = 1000)
- {
- if ($chunkSize < 1) {
- throw new InvalidArgumentException('The chunk size should be at least 1');
- }
- $this->enforceOrderBy();
- return LazyCollection::make(function () use ($chunkSize) {
- $page = 1;
- while (true) {
- $results = $this->forPage($page++, $chunkSize)->get();
- foreach ($results as $result) {
- yield $result;
- }
- if ($results->count() < $chunkSize) {
- return;
- }
- }
- });
- }
- /**
- * Query lazily, by chunking the results of a query by comparing IDs.
- *
- * @param int $chunkSize
- * @param string|null $column
- * @param string|null $alias
- * @return \Illuminate\Support\LazyCollection
- *
- * @throws \InvalidArgumentException
- */
- public function lazyById($chunkSize = 1000, $column = null, $alias = null)
- {
- return $this->orderedLazyById($chunkSize, $column, $alias);
- }
- /**
- * Query lazily, by chunking the results of a query by comparing IDs in descending order.
- *
- * @param int $chunkSize
- * @param string|null $column
- * @param string|null $alias
- * @return \Illuminate\Support\LazyCollection
- *
- * @throws \InvalidArgumentException
- */
- public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
- {
- return $this->orderedLazyById($chunkSize, $column, $alias, true);
- }
- /**
- * Query lazily, by chunking the results of a query by comparing IDs in a given order.
- *
- * @param int $chunkSize
- * @param string|null $column
- * @param string|null $alias
- * @param bool $descending
- * @return \Illuminate\Support\LazyCollection
- *
- * @throws \InvalidArgumentException
- */
- protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
- {
- if ($chunkSize < 1) {
- throw new InvalidArgumentException('The chunk size should be at least 1');
- }
- $column ??= $this->defaultKeyName();
- $alias ??= $column;
- return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) {
- $lastId = null;
- while (true) {
- $clone = clone $this;
- if ($descending) {
- $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
- } else {
- $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
- }
- foreach ($results as $result) {
- yield $result;
- }
- if ($results->count() < $chunkSize) {
- return;
- }
- $lastId = $results->last()->{$alias};
- if ($lastId === null) {
- throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
- }
- }
- });
- }
- /**
- * Execute the query and get the first result.
- *
- * @param array|string $columns
- * @return \Illuminate\Database\Eloquent\Model|object|static|null
- */
- public function first($columns = ['*'])
- {
- return $this->take(1)->get($columns)->first();
- }
- /**
- * Execute the query and get the first result if it's the sole matching record.
- *
- * @param array|string $columns
- * @return \Illuminate\Database\Eloquent\Model|object|static|null
- *
- * @throws \Illuminate\Database\RecordsNotFoundException
- * @throws \Illuminate\Database\MultipleRecordsFoundException
- */
- public function sole($columns = ['*'])
- {
- $result = $this->take(2)->get($columns);
- $count = $result->count();
- if ($count === 0) {
- throw new RecordsNotFoundException;
- }
- if ($count > 1) {
- throw new MultipleRecordsFoundException($count);
- }
- return $result->first();
- }
- /**
- * Paginate the given query using a cursor paginator.
- *
- * @param int $perPage
- * @param array|string $columns
- * @param string $cursorName
- * @param \Illuminate\Pagination\Cursor|string|null $cursor
- * @return \Illuminate\Contracts\Pagination\CursorPaginator
- */
- protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
- {
- if (! $cursor instanceof Cursor) {
- $cursor = is_string($cursor)
- ? Cursor::fromEncoded($cursor)
- : CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
- }
- $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
- if (! is_null($cursor)) {
- // Reset the union bindings so we can add the cursor where in the correct position...
- $this->setBindings([], 'union');
- $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
- $unionBuilders = $builder->getUnionBuilders();
- if (! is_null($previousColumn)) {
- $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
- $builder->where(
- Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
- '=',
- $cursor->parameter($previousColumn)
- );
- $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
- $unionBuilder->where(
- $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
- '=',
- $cursor->parameter($previousColumn)
- );
- $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
- });
- }
- $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
- ['column' => $column, 'direction' => $direction] = $orders[$i];
- $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
- $secondBuilder->where(
- Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
- $direction === 'asc' ? '>' : '<',
- $cursor->parameter($column)
- );
- if ($i < $orders->count() - 1) {
- $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
- $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
- });
- }
- $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
- $unionWheres = $unionBuilder->getRawBindings()['where'];
- $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
- $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
- $unionBuilder->where(
- $originalColumn,
- $direction === 'asc' ? '>' : '<',
- $cursor->parameter($column)
- );
- if ($i < $orders->count() - 1) {
- $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
- $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
- });
- }
- $this->addBinding($unionWheres, 'union');
- $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
- });
- });
- });
- };
- $addCursorConditions($this, null, null, 0);
- }
- $this->limit($perPage + 1);
- return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
- 'path' => Paginator::resolveCurrentPath(),
- 'cursorName' => $cursorName,
- 'parameters' => $orders->pluck('column')->toArray(),
- ]);
- }
- /**
- * Get the original column name of the given column, without any aliasing.
- *
- * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
- * @param string $parameter
- * @return string
- */
- protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
- {
- $columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
- if (! is_null($columns)) {
- foreach ($columns as $column) {
- if (($position = strripos($column, ' as ')) !== false) {
- $original = substr($column, 0, $position);
- $alias = substr($column, $position + 4);
- if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
- return $original;
- }
- }
- }
- }
- return $parameter;
- }
- /**
- * Create a new length-aware paginator instance.
- *
- * @param \Illuminate\Support\Collection $items
- * @param int $total
- * @param int $perPage
- * @param int $currentPage
- * @param array $options
- * @return \Illuminate\Pagination\LengthAwarePaginator
- */
- protected function paginator($items, $total, $perPage, $currentPage, $options)
- {
- return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
- 'items', 'total', 'perPage', 'currentPage', 'options'
- ));
- }
- /**
- * Create a new simple paginator instance.
- *
- * @param \Illuminate\Support\Collection $items
- * @param int $perPage
- * @param int $currentPage
- * @param array $options
- * @return \Illuminate\Pagination\Paginator
- */
- protected function simplePaginator($items, $perPage, $currentPage, $options)
- {
- return Container::getInstance()->makeWith(Paginator::class, compact(
- 'items', 'perPage', 'currentPage', 'options'
- ));
- }
- /**
- * Create a new cursor paginator instance.
- *
- * @param \Illuminate\Support\Collection $items
- * @param int $perPage
- * @param \Illuminate\Pagination\Cursor $cursor
- * @param array $options
- * @return \Illuminate\Pagination\CursorPaginator
- */
- protected function cursorPaginator($items, $perPage, $cursor, $options)
- {
- return Container::getInstance()->makeWith(CursorPaginator::class, compact(
- 'items', 'perPage', 'cursor', 'options'
- ));
- }
- /**
- * Pass the query to a given callback.
- *
- * @param callable($this): mixed $callback
- * @return $this
- */
- public function tap($callback)
- {
- $callback($this);
- return $this;
- }
- }
|