BuildsQueries.php
TLDR
The file BuildsQueries.php
contains a trait that provides methods for handling queries in the Illuminate Database package.
Methods
chunk
This method chunks the results of a query by executing the query for each page and calling a callback with the current chunk of results.
chunkMap
This method runs a map over each item while chunking. It executes the chunk
method and then pushes each item through a callback.
each
This method executes a callback over each item while chunking. It executes the chunk
method and then iterates over each result, calling the callback with the value and key of each item.
chunkById
This method chunks the results of a query by comparing IDs. It is a wrapper for the orderedChunkById
method.
chunkByIdDesc
This method chunks the results of a query by comparing IDs in descending order. It is a wrapper for the orderedChunkById
method.
orderedChunkById
This method chunks the results of a query by comparing IDs in a given order. It executes the query for each page and calls a callback with the current chunk of results. The order of the results is determined by comparing the IDs.
eachById
This method executes a callback over each item while chunking by ID. It is a wrapper for the chunkById
method.
lazy
This method allows lazy querying by chunks of the given size. It returns a LazyCollection
that executes the query and yields results as they are requested.
lazyById
This method allows lazy querying by chunking the results of a query by comparing IDs. It is a wrapper for the orderedLazyById
method.
lazyByIdDesc
This method allows lazy querying by chunking the results of a query by comparing IDs in descending order. It is a wrapper for the orderedLazyById
method.
orderedLazyById
This method allows lazy querying by chunking the results of a query by comparing IDs in a given order. It returns a LazyCollection
that executes the query and yields results as they are requested. The order of the results is determined by comparing the IDs.
first
This method executes the query and returns the first result.
sole
This method executes the query and returns the first result if it's the sole matching record. If no records are found, it throws a RecordsNotFoundException
. If multiple records are found, it throws a MultipleRecordsFoundException
.
paginateUsingCursor
This method paginates the given query using a cursor paginator. It takes a cursor object or a cursor name and value and returns a CursorPaginator
instance.
tap
This method passes the query to a given callback.
Classes
None
<?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
*/
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)) {
$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect();
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($this, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}
$builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
$addCursorConditions($builder, $column, $i + 1);
});
}
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($this, $column),
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);
if ($i < $orders->count() - 1) {
$unionBuilder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
$addCursorConditions($builder, $column, $i + 1);
});
}
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
});
});
};
$addCursorConditions($this, 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 $callback
* @return $this
*/
public function tap($callback)
{
$callback($this);
return $this;
}
}