<?php
namespace Illuminate\Database\Eloquent;
use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Support\Collection as BaseCollection;
class Collection extends BaseCollection implements QueueableCollection
{
/**
* Find a model in the collection by key.
*
* @param mixed $key
* @param mixed $default
* @return \Illuminate\Database\Eloquent\Model|static
*/
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, function ($model) use ($key) {
return $model->getKey() == $key;
}, $default);
}
/**
* Load a set of relationships onto the collection.
*
* @param array|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 relationships onto the collection if they are not already eager loaded.
*
* @param array|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 = array_combine($segments, $segments);
if (is_callable($value)) {
$path[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(Collection $models, array $path)
{
$relation = array_splice($path, 0, 1);
$name = explode(':', key($relation))[0];
if (is_string(reset($relation))) {
$relation = reset($relation);
}
$models->filter(function ($model) use ($name) {
return ! is_null($model) && ! $model->relationLoaded($name);
})->load($relation);
if (empty($path)) {
return;
}
$models = $models->pluck($name);
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 $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$this->pluck($relation)
->filter()
->groupBy(function ($model) {
return get_class($model);
})
->filter(function ($models, $className) use ($relations) {
return Arr::has($relations, $className);
})
->each(function ($models, $className) use ($relations) {
$className::with($relations[$className])
->eagerLoadRelations($models->all());
});
return $this;
}
/**
* Add an item to the collection.
*
* @param mixed $item
* @return $this
*/
public function add($item)
{
$this->items[] = $item;
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param mixed $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(function ($model) use ($key) {
return $model->is($key);
});
}
return parent::contains(function ($model) use ($key) {
return $model->getKey() == $key;
});
}
/**
* Get the array of primary keys.
*
* @return array
*/
public function modelKeys()
{
return array_map(function ($model) {
return $model->getKey();
}, $this->items);
}
/**
* Merge the collection with the given items.
*
* @param \ArrayAccess|array $items
* @return static
*/
public function merge($items)
{
$dictionary = $this->getDictionary();
foreach ($items as $item) {
$dictionary[$item->getKey()] = $item;
}
return new static(array_values($dictionary));
}
/**
* Run a map over each of the items.
*
* @param callable $callback
* @return \Illuminate\Support\Collection|static
*/
public function map(callable $callback)
{
$result = parent::map($callback);
return $result->contains(function ($item) {
return ! $item instanceof Model;
}) ? $result->toBase() : $result;
}
/**
* Reload a fresh model instance from the database for all the entities.
*
* @param array|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->map(function ($model) use ($freshModels) {
return $model->exists && isset($freshModels[$model->getKey()])
? $freshModels[$model->getKey()] : null;
});
}
/**
* Diff the collection with the given items.
*
* @param \ArrayAccess|array $items
* @return static
*/
public function diff($items)
{
$diff = new static;
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
if (! isset($dictionary[$item->getKey()])) {
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given items.
*
* @param \ArrayAccess|array $items
* @return static
*/
public function intersect($items)
{
$intersect = new static;
$dictionary = $this->getDictionary($items);
foreach ($this->items as $item) {
if (isset($dictionary[$item->getKey()])) {
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @param string|callable|null $key
* @param bool $strict
* @return static|\Illuminate\Support\Collection
*/
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 mixed $keys
* @return static
*/
public function only($keys)
{
if (is_null($keys)) {
return new static($this->items);
}
$dictionary = Arr::only($this->getDictionary(), $keys);
return new static(array_values($dictionary));
}
/**
* Returns all models in the collection except the models with specified keys.
*
* @param mixed $keys
* @return static
*/
public function except($keys)
{
$dictionary = Arr::except($this->getDictionary(), $keys);
return new static(array_values($dictionary));
}
/**
* Make the given, typically visible, attributes hidden across the entire collection.
*
* @param array|string $attributes
* @return $this
*/
public function makeHidden($attributes)
{
return $this->each(function ($model) use ($attributes) {
$model->addHidden($attributes);
});
}
/**
* Make the given, typically hidden, attributes visible across the entire collection.
*
* @param array|string $attributes
* @return $this
*/
public function makeVisible($attributes)
{
return $this->each(function ($model) use ($attributes) {
$model->makeVisible($attributes);
});
}
/**
* Get a dictionary keyed by primary keys.
*
* @param \ArrayAccess|array|null $items
* @return array
*/
public function getDictionary($items = null)
{
$items = is_null($items) ? $this->items : $items;
$dictionary = [];
foreach ($items as $value) {
$dictionary[$value->getKey()] = $value;
}
return $dictionary;
}
/**
* The following methods are intercepted to always return base collections.
*/
/**
* Get an array with the values of a given key.
*
* @param string $value
* @param string|null $key
* @return \Illuminate\Support\Collection
*/
public function pluck($value, $key = null)
{
return $this->toBase()->pluck($value, $key);
}
/**
* Get the keys of the collection items.
*
* @return \Illuminate\Support\Collection
*/
public function keys()
{
return $this->toBase()->keys();
}
/**
* Zip the collection together with one or more arrays.
*
* @param mixed ...$items
* @return \Illuminate\Support\Collection
*/
public function zip($items)
{
return call_user_func_array([$this->toBase(), 'zip'], func_get_args());
}
/**
* Collapse the collection of items into a single array.
*
* @return \Illuminate\Support\Collection
*/
public function collapse()
{
return $this->toBase()->collapse();
}
/**
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return \Illuminate\Support\Collection
*/
public function flatten($depth = INF)
{
return $this->toBase()->flatten($depth);
}
/**
* Flip the items in the collection.
*
* @return \Illuminate\Support\Collection
*/
public function flip()
{
return $this->toBase()->flip();
}
/**
* Pad collection to the specified length with a value.
*
* @param int $size
* @param mixed $value
* @return \Illuminate\Support\Collection
*/
public function pad($size, $value)
{
return $this->toBase()->pad($size, $value);
}
/**
* Get the type of the entities being queued.
*
* @return string|null
* @throws \LogicException
*/
public function getQueueableClass()
{
if ($this->isEmpty()) {
return;
}
$class = get_class($this->first());
$this->each(function ($model) use ($class) {
if (get_class($model) !== $class) {
throw new LogicException('Queueing collections with multiple model types is not supported.');
}
});
return $class;
}
/**
* Get the identifiers for all of the entities.
*
* @return array
*/
public function getQueueableIds()
{
if ($this->isEmpty()) {
return [];
}
return $this->first() instanceof Pivot
? $this->map->getQueueableId()->all()
: $this->modelKeys();
}
/**
* Get the relationships of the entities being queued.
*
* @return array
*/
public function getQueueableRelations()
{
return $this->isNotEmpty() ? $this->first()->getQueueableRelations() : [];
}
/**
* 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;
}
}