Collection.php
TLDR
The Collection
class extends the Illuminate\Support\Collection
class and implements the Illuminate\Contracts\Queue\QueueableCollection
interface. It provides additional functionality specific to working with collections of Eloquent models.
Methods
find
Find a model in the collection by key.
load
Load a set of relationships onto the collection.
loadAggregate
Load a set of aggregations over relationship's column onto the collection.
loadCount
Load a set of relationship counts onto the collection.
loadMax
Load a set of relationship's max column values onto the collection.
loadMin
Load a set of relationship's min column values onto the collection.
loadSum
Load a set of relationship's column summations onto the collection.
loadAvg
Load a set of relationship's average column values onto the collection.
loadExists
Load a set of related existences onto the collection.
loadMissing
Load a set of relationships onto the collection if they are not already eager loaded.
loadMorph
Load a set of relationships onto the mixed relationship collection.
loadMorphCount
Load a set of relationship counts onto the mixed relationship collection.
contains
Determine if a key exists in the collection.
modelKeys
Get the array of primary keys.
merge
Merge the collection with the given items.
map
Run a map over each of the items.
mapWithKeys
Run an associative map over each of the items.
fresh
Reload a fresh model instance from the database for all the entities.
diff
Diff the collection with the given items.
intersect
Intersect the collection with the given items.
unique
Return only unique items from the collection.
only
Returns only the models from the collection with the specified keys.
except
Returns all models in the collection except the models with specified keys.
makeHidden
Make the given, typically visible, attributes hidden across the entire collection.
makeVisible
Make the given, typically hidden, attributes visible across the entire collection.
setVisible
Set the visible attributes across the entire collection.
setHidden
Set the hidden attributes across the entire collection.
append
Append an attribute across the entire collection.
getDictionary
Get a dictionary keyed by primary keys.
countBy
Count the number of items in the collection by a field or using a callback.
collapse
Collapse the collection of items into a single array.
flatten
Get a flattened array of the items in the collection.
flip
Flip the items in the collection.
keys
Get the keys of the collection items.
pad
Pad collection to the specified length with a value.
pluck
Get an array with the values of a given key.
zip
Zip the collection together with one or more arrays.
duplicateComparator
Get the comparison function to detect duplicates.
getQueueableClass
Get the type of the entities being queued.
getQueueableModelClass
Get the queueable class name for the given model.
getQueueableIds
Get the identifiers for all of the entities.
getQueueableRelations
Get the relationships of the entities being queued.
getQueueableConnection
Get the connection of the entities being queued.
toQuery
Get the Eloquent query builder from the collection.
END
<?php
namespace Illuminate\Database\Eloquent;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection as BaseCollection;
use LogicException;
/**
* @template TKey of array-key
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Support\Collection<TKey, TModel>
*/
class Collection extends BaseCollection implements QueueableCollection
{
use InteractsWithDictionary;
/**
* Find a model in the collection by key.
*
* @template TFindDefault
*
* @param mixed $key
* @param TFindDefault $default
* @return static<TKey, TModel>|TModel|TFindDefault
*/
public function find($key, $default = null)
{
if ($key instanceof Model) {
$key = $key->getKey();
}
if ($key instanceof Arrayable) {
$key = $key->toArray();
}
if (is_array($key)) {
if ($this->isEmpty()) {
return new static;
}
return $this->whereIn($this->first()->getKeyName(), $key);
}
return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default);
}
/**
* Load a set of relationships onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function load($relations)
{
if ($this->isNotEmpty()) {
if (is_string($relations)) {
$relations = func_get_args();
}
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
$this->items = $query->eagerLoadRelations($this->items);
}
return $this;
}
/**
* Load a set of aggregations over relationship's column onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @param string|null $function
* @return $this
*/
public function loadAggregate($relations, $column, $function = null)
{
if ($this->isEmpty()) {
return $this;
}
$models = $this->first()->newModelQuery()
->whereKey($this->modelKeys())
->select($this->first()->getKeyName())
->withAggregate($relations, $column, $function)
->get()
->keyBy($this->first()->getKeyName());
$attributes = Arr::except(
array_keys($models->first()->getAttributes()),
$models->first()->getKeyName()
);
$this->each(function ($model) use ($models, $attributes) {
$extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);
$model->forceFill($extraAttributes)
->syncOriginalAttributes($attributes)
->mergeCasts($models->get($model->getKey())->getCasts());
});
return $this;
}
/**
* Load a set of relationship counts onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function loadCount($relations)
{
return $this->loadAggregate($relations, '*', 'count');
}
/**
* Load a set of relationship's max column values onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadMax($relations, $column)
{
return $this->loadAggregate($relations, $column, 'max');
}
/**
* Load a set of relationship's min column values onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadMin($relations, $column)
{
return $this->loadAggregate($relations, $column, 'min');
}
/**
* Load a set of relationship's column summations onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadSum($relations, $column)
{
return $this->loadAggregate($relations, $column, 'sum');
}
/**
* Load a set of relationship's average column values onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @param string $column
* @return $this
*/
public function loadAvg($relations, $column)
{
return $this->loadAggregate($relations, $column, 'avg');
}
/**
* Load a set of related existences onto the collection.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function loadExists($relations)
{
return $this->loadAggregate($relations, '*', 'exists');
}
/**
* Load a set of relationships onto the collection if they are not already eager loaded.
*
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
* @return $this
*/
public function loadMissing($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
foreach ($relations as $key => $value) {
if (is_numeric($key)) {
$key = $value;
}
$segments = explode('.', explode(':', $key)[0]);
if (str_contains($key, ':')) {
$segments[count($segments) - 1] .= ':'.explode(':', $key)[1];
}
$path = [];
foreach ($segments as $segment) {
$path[] = [$segment => $segment];
}
if (is_callable($value)) {
$path[count($segments) - 1][end($segments)] = $value;
}
$this->loadMissingRelation($this, $path);
}
return $this;
}
/**
* Load a relationship path if it is not already eager loaded.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @param array $path
* @return void
*/
protected function loadMissingRelation(self $models, array $path)
{
$relation = array_shift($path);
$name = explode(':', key($relation))[0];
if (is_string(reset($relation))) {
$relation = reset($relation);
}
$models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation);
if (empty($path)) {
return;
}
$models = $models->pluck($name)->whereNotNull();
if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}
$this->loadMissingRelation(new static($models), $path);
}
/**
* Load a set of relationships onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? []));
return $this;
}
/**
* Load a set of relationship counts onto the mixed relationship collection.
*
* @param string $relation
* @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
* @return $this
*/
public function loadMorphCount($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? []));
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param (callable(TModel, TKey): bool)|TModel|string|int $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function contains($key, $operator = null, $value = null)
{
if (func_num_args() > 1 || $this->useAsCallable($key)) {
return parent::contains(...func_get_args());
}
if ($key instanceof Model) {
return parent::contains(fn ($model) => $model->is($key));
}
return parent::contains(fn ($model) => $model->getKey() == $key);
}
/**
* Get the array of primary keys.
*
* @return array<int, array-key>
*/
public function modelKeys()
{
return array_map(fn ($model) => $model->getKey(), $this->items);
}
/**
* Merge the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function merge($items)
{
$dictionary = $this->getDictionary();
foreach ($items as $item) {
$dictionary[$this->getDictionaryKey($item->getKey())] = $item;
}
return new static(array_values($dictionary));
}
/**
* Run a map over each of the items.
*
* @template TMapValue
*
* @param callable(TModel, TKey): TMapValue $callback
* @return \Illuminate\Support\Collection<TKey, TMapValue>|static<TKey, TMapValue>
*/
public function map(callable $callback)
{
$result = parent::map($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Run an associative map over each of the items.
*
* The callback should return an associative array with a single key / value pair.
*
* @template TMapWithKeysKey of array-key
* @template TMapWithKeysValue
*
* @param callable(TModel, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return \Illuminate\Support\Collection<TMapWithKeysKey, TMapWithKeysValue>|static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
$result = parent::mapWithKeys($callback);
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
* Reload a fresh model instance from the database for all the entities.
*
* @param array<array-key, string>|string $with
* @return static
*/
public function fresh($with = [])
{
if ($this->isEmpty()) {
return new static;
}
$model = $this->first();
$freshModels = $model->newQueryWithoutScopes()
->with(is_string($with) ? func_get_args() : $with)
->whereIn($model->getKeyName(), $this->modelKeys())
->get()
->getDictionary();
return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()]))
->map(fn ($model) => $freshModels[$model->getKey()]);
}
/**
* Diff the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function diff($items)
{
$diff = new static;
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
if (! isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given items.
*
* @param iterable<array-key, TModel> $items
* @return static
*/
public function intersect($items)
{
$intersect = new static;
if (empty($items)) {
return $intersect;
}
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
if (isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @param (callable(TModel, TKey): mixed)|string|null $key
* @param bool $strict
* @return static<int, TModel>
*/
public function unique($key = null, $strict = false)
{
if (! is_null($key)) {
return parent::unique($key, $strict);
}
return new static(array_values($this->getDictionary()));
}
/**
* Returns only the models from the collection with the specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function only($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::only($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Returns all models in the collection except the models with specified keys.
*
* @param array<array-key, mixed>|null $keys
* @return static<int, TModel>
*/
public function except($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::except($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
return new static(array_values($dictionary));
}
/**
* Make the given, typically visible, attributes hidden across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeHidden($attributes)
{
return $this->each->makeHidden($attributes);
}
/**
* Make the given, typically hidden, attributes visible across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function makeVisible($attributes)
{
return $this->each->makeVisible($attributes);
}
/**
* Set the visible attributes across the entire collection.
*
* @param array<int, string> $visible
* @return $this
*/
public function setVisible($visible)
{
return $this->each->setVisible($visible);
}
/**
* Set the hidden attributes across the entire collection.
*
* @param array<int, string> $hidden
* @return $this
*/
public function setHidden($hidden)
{
return $this->each->setHidden($hidden);
}
/**
* Append an attribute across the entire collection.
*
* @param array<array-key, string>|string $attributes
* @return $this
*/
public function append($attributes)
{
return $this->each->append($attributes);
}
/**
* Get a dictionary keyed by primary keys.
*
* @param iterable<array-key, TModel>|null $items
* @return array<array-key, TModel>
*/
public function getDictionary($items = null)
{
$items = is_null($items) ? $this->items : $items;
$dictionary = [];
foreach ($items as $value) {
$dictionary[$this->getDictionaryKey($value->getKey())] = $value;
}
return $dictionary;
}
/**
* The following methods are intercepted to always return base collections.
*/
/**
* Count the number of items in the collection by a field or using a callback.
*
* @param (callable(TModel, TKey): array-key)|string|null $countBy
* @return \Illuminate\Support\Collection<array-key, int>
*/
public function countBy($countBy = null)
{
return $this->toBase()->countBy($countBy);
}
/**
* Collapse the collection of items into a single array.
*
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function collapse()
{
return $this->toBase()->collapse();
}
/**
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return \Illuminate\Support\Collection<int, mixed>
*/
public function flatten($depth = INF)
{
return $this->toBase()->flatten($depth);
}
/**
* Flip the items in the collection.
*
* @return \Illuminate\Support\Collection<TModel, TKey>
*/
public function flip()
{
return $this->toBase()->flip();
}
/**
* Get the keys of the collection items.
*
* @return \Illuminate\Support\Collection<int, TKey>
*/
public function keys()
{
return $this->toBase()->keys();
}
/**
* Pad collection to the specified length with a value.
*
* @template TPadValue
*
* @param int $size
* @param TPadValue $value
* @return \Illuminate\Support\Collection<int, TModel|TPadValue>
*/
public function pad($size, $value)
{
return $this->toBase()->pad($size, $value);
}
/**
* Get an array with the values of a given key.
*
* @param string|array<array-key, string> $value
* @param string|null $key
* @return \Illuminate\Support\Collection<array-key, mixed>
*/
public function pluck($value, $key = null)
{
return $this->toBase()->pluck($value, $key);
}
/**
* Zip the collection together with one or more arrays.
*
* @template TZipValue
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
*/
public function zip($items)
{
return $this->toBase()->zip(...func_get_args());
}
/**
* Get the comparison function to detect duplicates.
*
* @param bool $strict
* @return callable(TModel, TModel): bool
*/
protected function duplicateComparator($strict)
{
return fn ($a, $b) => $a->is($b);
}
/**
* Get the type of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->isEmpty()) {
return;
}
$class = $this->getQueueableModelClass($this->first());
$this->each(function ($model) use ($class) {
if ($this->getQueueableModelClass($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
return $class;
}
/**
* Get the queueable class name for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getQueueableModelClass($model)
{
return method_exists($model, 'getQueueableClassName')
? $model->getQueueableClassName()
: get_class($model);
}
/**
* Get the identifiers for all of the entities.
*
* @return array<int, mixed>
*/
public function getQueueableIds()
{
if ($this->isEmpty()) {
return [];
}
return $this->first() instanceof QueueableEntity
? $this->map->getQueueableId()->all()
: $this->modelKeys();
}
/**
* Get the relationships of the entities being queued.
*
* @return array<int, string>
*/
public function getQueueableRelations()
{
if ($this->isEmpty()) {
return [];
}
$relations = $this->map->getQueueableRelations()->all();
if (count($relations) === 0 || $relations === [[]]) {
return [];
} elseif (count($relations) === 1) {
return reset($relations);
} else {
return array_intersect(...array_values($relations));
}
}
/**
* Get the connection of the entities being queued.
*
* @return string|null
*
* @throws \LogicException
*/
public function getQueueableConnection()
{
if ($this->isEmpty()) {
return;
}
$connection = $this->first()->getConnectionName();
$this->each(function ($model) use ($connection) {
if ($model->getConnectionName() !== $connection) {
throw new LogicException('Queueing collections with multiple model connections is not supported.');
}
});
return $connection;
}
/**
* Get the Eloquent query builder from the collection.
*
* @return \Illuminate\Database\Eloquent\Builder
*
* @throws \LogicException
*/
public function toQuery()
{
$model = $this->first();
if (! $model) {
throw new LogicException('Unable to create query for empty collection.');
}
$class = get_class($model);
if ($this->filter(fn ($model) => ! $model instanceof $class)->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}
return $model->newModelQuery()->whereKey($this->modelKeys());
}
}