From d35a07b935a2e2881266643509181839d61db885 Mon Sep 17 00:00:00 2001 From: agis Date: Fri, 11 Sep 2026 14:21:14 +0700 Subject: [PATCH] feat(support): bring Collection/Arr to L13 shape (task 3.3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy the v13 collection-family into the fork's Support tree so the Collection surface matches Laravel 13, sourced verbatim from the clean v13.30.1 tag (git show) — NOT the local ../framework working tree, which sits on an inconsistent merge branch. New files: Enumerable, Traits/EnumeratesValues, LazyCollection, HigherOrderCollectionProxy, Item/MultipleItemsFoundException, functions.php (enum_value), Contracts/Support/CanBeEscapedWhenCastToString, and a minimal SortDirection enum reconstructed because v13 references it from Collection::sort*() but ships no defining file. Two deliberate deviations from verbatim copy: - strip `TransformsToResourceCollection` from Collection — it pulls in Http\Resources and would undo the SCC-1 decoupling from task 3.3a. - keep the 4.2 holdovers the fork's Database layer still calls (Collection::lists()/fetch(), Arr::build()/fetch()); removed in task 2.5. Arr: keep the fork class (first/last already v13 value-first), add from/mapWithKeys/partition/select, and flip Arr::where to v13 value-first semantics (fix its whereNotNull + UrlGenerator callers). Eloquent\Collection (only Collection subclass): widen contains()/unique() and delegate the v13 args to parent; drop the now-redundant max()/min() overrides (base v13 handles model collections via data_get). Update SupportCollectionTest/SupportArrTest assertions to v13 semantics (immutable sort, key-preserving chunk/takeLast, keys()->Collection, random-on-empty throws, toJson via jsonSerialize, offsetExists via isset) and MorphTo::gatherKeysByType (->values()) for v13 map key-preservation. Full suite green (1641); SCC-1 stays broken; ratchet baseline tightened (macroable_trait 14->12, legacy_contracts 50->37). Co-Authored-By: Claude Opus 4.8 (1M context) --- ci/convergence-baseline.txt | 4 +- composer.json | 3 +- .../Support/CanBeEscapedWhenCastToString.php | 14 + .../Database/Eloquent/Collection.php | 42 +- .../Database/Eloquent/Relations/MorphTo.php | 2 +- src/Illuminate/Routing/UrlGenerator.php | 4 +- src/Illuminate/Support/Arr.php | 116 +- src/Illuminate/Support/Collection.php | 3001 +++++++++++------ src/Illuminate/Support/Enumerable.php | 1379 ++++++++ .../Support/HigherOrderCollectionProxy.php | 69 + .../Support/ItemNotFoundException.php | 9 + src/Illuminate/Support/LazyCollection.php | 1978 +++++++++++ .../Support/MultipleItemsFoundException.php | 39 + src/Illuminate/Support/SortDirection.php | 12 + .../Support/Traits/EnumeratesValues.php | 1244 +++++++ src/Illuminate/Support/functions.php | 27 + tests/Support/SupportArrTest.php | 6 +- tests/Support/SupportCollectionTest.php | 35 +- 18 files changed, 6959 insertions(+), 1025 deletions(-) create mode 100644 src/Illuminate/Contracts/Support/CanBeEscapedWhenCastToString.php create mode 100644 src/Illuminate/Support/Enumerable.php create mode 100644 src/Illuminate/Support/HigherOrderCollectionProxy.php create mode 100644 src/Illuminate/Support/ItemNotFoundException.php create mode 100644 src/Illuminate/Support/LazyCollection.php create mode 100644 src/Illuminate/Support/MultipleItemsFoundException.php create mode 100644 src/Illuminate/Support/SortDirection.php create mode 100644 src/Illuminate/Support/Traits/EnumeratesValues.php create mode 100644 src/Illuminate/Support/functions.php diff --git a/ci/convergence-baseline.txt b/ci/convergence-baseline.txt index 6a9c9fa8..557da432 100644 --- a/ci/convergence-baseline.txt +++ b/ci/convergence-baseline.txt @@ -6,8 +6,8 @@ event_fire=0 config_getEnvironment=3 where_raw=0 eloquent_lists=0 -macroable_trait=14 +macroable_trait=12 softdeleting_trait=2 -legacy_contracts=50 +legacy_contracts=37 pagination_getters=0 route_filters=4 diff --git a/composer.json b/composer.json index 2d2b95d1..9f748608 100755 --- a/composer.json +++ b/composer.json @@ -79,7 +79,8 @@ "src/Illuminate/Queue/IlluminateQueueClosure.php" ], "files": [ - "src/Illuminate/Support/helpers.php" + "src/Illuminate/Support/helpers.php", + "src/Illuminate/Support/functions.php" ], "psr-4": { "L4\\": "src/L4/", diff --git a/src/Illuminate/Contracts/Support/CanBeEscapedWhenCastToString.php b/src/Illuminate/Contracts/Support/CanBeEscapedWhenCastToString.php new file mode 100644 index 00000000..e1be6fef --- /dev/null +++ b/src/Illuminate/Contracts/Support/CanBeEscapedWhenCastToString.php @@ -0,0 +1,14 @@ + 1) + { + return parent::contains($key, $operator, $value); + } + return ! is_null($this->find($key)); } @@ -83,34 +88,6 @@ public function fetch($key) return new static(array_fetch($this->toArray(), $key)); } - /** - * Get the max value of a given key. - * - * @param string $key - * @return mixed - */ - public function max($key) - { - return $this->reduce(function($result, $item) use ($key) - { - return (is_null($result) || $item->{$key} > $result) ? $item->{$key} : $result; - }); - } - - /** - * Get the min value of a given key. - * - * @param string $key - * @return mixed - */ - public function min($key) - { - return $this->reduce(function($result, $item) use ($key) - { - return (is_null($result) || $item->{$key} < $result) ? $item->{$key} : $result; - }); - } - /** * Get the array of primary keys * @@ -194,8 +171,13 @@ public function intersect($items) * @return static */ #[\Override] - public function unique() + public function unique($key = null, $strict = false) { + if ( ! is_null($key)) + { + return parent::unique($key, $strict); + } + $dictionary = $this->getDictionary(); return new static(array_values($dictionary)); diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index 65c23418..694b04ab 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -184,7 +184,7 @@ protected function gatherKeysByType($type) { return head($models)->{$foreign}; - })->unique(); + })->unique()->values(); } /** diff --git a/src/Illuminate/Routing/UrlGenerator.php b/src/Illuminate/Routing/UrlGenerator.php index e6a0dde0..5142ad30 100755 --- a/src/Illuminate/Routing/UrlGenerator.php +++ b/src/Illuminate/Routing/UrlGenerator.php @@ -361,7 +361,7 @@ protected function getRouteQueryString(array $parameters) */ protected function getStringParameters(array $parameters) { - return array_where($parameters, function($k, $v) { return is_string($k); }); + return array_where($parameters, function($v, $k) { return is_string($k); }); } /** @@ -372,7 +372,7 @@ protected function getStringParameters(array $parameters) */ protected function getNumericParameters(array $parameters) { - return array_where($parameters, function($k, $v) { return is_numeric($k); }); + return array_where($parameters, function($v, $k) { return is_numeric($k); }); } /** diff --git a/src/Illuminate/Support/Arr.php b/src/Illuminate/Support/Arr.php index d9c25428..1071c58a 100755 --- a/src/Illuminate/Support/Arr.php +++ b/src/Illuminate/Support/Arr.php @@ -3,13 +3,112 @@ use ArgumentCountError; use ArrayAccess; use Closure; +use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\MacroableTrait; use InvalidArgumentException; +use JsonSerializable; +use Traversable; +use WeakMap; class Arr { use MacroableTrait; + /** + * Get an array from the given items. + * + * @param mixed $items + * @return array + */ + public static function from($items) + { + return match (true) { + is_array($items) => $items, + $items instanceof Enumerable => $items->all(), + $items instanceof Arrayable => $items->toArray(), + $items instanceof WeakMap => iterator_to_array($items, false), + $items instanceof Traversable => iterator_to_array($items), + $items instanceof Jsonable => json_decode($items->toJson(), true), + $items instanceof JsonSerializable => (array) $items->jsonSerialize(), + is_object($items) => (array) $items, + default => throw new InvalidArgumentException('Items cannot be represented by a scalar value.'), + }; + } + + /** + * Run an associative map over each of the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @param array $array + * @param callable $callback + * @return array + */ + public static function mapWithKeys(array $array, callable $callback) + { + $result = []; + + foreach ($array as $key => $value) { + $assoc = $callback($value, $key); + + foreach ($assoc as $mapKey => $mapValue) { + $result[$mapKey] = $mapValue; + } + } + + return $result; + } + + /** + * Partition the array into two arrays using the given callback. + * + * @param array $array + * @param callable $callback + * @return array + */ + public static function partition($array, callable $callback) + { + $passed = []; + $failed = []; + + foreach ($array as $key => $item) { + if ($callback($item, $key)) { + $passed[$key] = $item; + } else { + $failed[$key] = $item; + } + } + + return [$passed, $failed]; + } + + /** + * Select an array of values from an array. + * + * @param array $array + * @param array|string $keys + * @return array + */ + public static function select($array, $keys) + { + $keys = static::wrap($keys); + + return static::map($array, function ($item) use ($keys) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + return $result; + }); + } + /** * Determine whether the given value is array accessible. * @@ -842,22 +941,15 @@ public static function toCssClasses(array $array): string } /** - * Filter the array using the given Closure. + * Filter the array using the given callback. * * @param array $array - * @param \Closure $callback + * @param callable $callback * @return array */ - public static function where($array, Closure $callback): array + public static function where($array, callable $callback): array { - $filtered = array(); - - foreach ($array as $key => $value) - { - if (call_user_func($callback, $key, $value)) $filtered[$key] = $value; - } - - return $filtered; + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); } /** @@ -868,7 +960,7 @@ public static function where($array, Closure $callback): array */ public static function whereNotNull($array) { - return static::where($array, function ($key, $value) { + return static::where($array, function ($value, $key) { return ! is_null($value); }); } diff --git a/src/Illuminate/Support/Collection.php b/src/Illuminate/Support/Collection.php index f7359001..01aec573 100755 --- a/src/Illuminate/Support/Collection.php +++ b/src/Illuminate/Support/Collection.php @@ -1,951 +1,2050 @@ -items = $items; - } - - /** - * Create a new collection instance if the value isn't one already. - * - * @param mixed $items - * @return static - */ - public static function make($items) - { - if (is_null($items)) return new static; - - if ($items instanceof Collection) return $items; - - return new static(is_array($items) ? $items : array($items)); - } - - /** - * Get all of the items in the collection. - * - * @return array - */ - public function all() - { - return $this->items; - } - - /** - * Collapse the collection items into a single array. - * - * @return static - */ - public function collapse() - { - $results = array(); - - foreach ($this->items as $values) - { - if ($values instanceof Collection) $values = $values->all(); - - $results = array_merge($results, $values); - } - - return new static($results); - } - - /** - * Determine if an item exists in the collection. - * - * @param mixed $value - * @return bool - */ - public function contains($value) - { - if ($value instanceof Closure) - { - return ! is_null($this->first($value)); - } - - return in_array($value, $this->items); - } - - /** - * Diff the collection with the given items. - * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return static - */ - public function diff($items) - { - return new static(array_diff($this->items, $this->getArrayableItems($items))); - } - - /** - * Execute a callback over each item. - * - * @param \Closure $callback - * @return $this - */ - public function each(Closure $callback) - { - array_map($callback, $this->items); - - return $this; - } - - /** - * Fetch a nested element of the collection. - * - * @param string $key - * @return static - */ - public function fetch($key) - { - return new static(array_fetch($this->items, $key)); - } - - /** - * Run a filter over each of the items. - * - * @param \Closure $callback - * @return static - */ - public function filter(Closure $callback) - { - return new static(array_filter($this->items, $callback)); - } - - /** - * Get the first item from the collection. - * - * @param \Closure $callback - * @param mixed $default - * @return mixed|null - */ - public function first(?Closure $callback = null, $default = null) - { - if (is_null($callback)) - { - return count($this->items) > 0 ? reset($this->items) : null; - } - - return Arr::first($this->items, $callback, $default); - } - - /** - * Get a flattened array of the items in the collection. - * - * @return static - */ - public function flatten() - { - return new static(array_flatten($this->items)); - } - - /** - * Flip the items in the collection. - * - * @return static - */ - public function flip() - { - return new static(array_flip($this->items)); - } - - /** - * Remove an item from the collection by key. - * - * @param mixed $key - * @return void - */ - public function forget($key) - { - unset($this->items[$key]); - } - - /** - * Get an item from the collection by key. - * - * @param mixed $key - * @param mixed $default - * @return mixed - */ - public function get($key, $default = null) - { - if ($this->offsetExists($key)) - { - return $this->items[$key]; - } - - return value($default); - } - - /** - * Group an associative array by a field or Closure value. - * - * @param callable|string $groupBy - * @return static - */ - public function groupBy($groupBy) - { - $results = array(); - - foreach ($this->items as $key => $value) - { - $results[$this->getGroupByKey($groupBy, $key, $value) ?? ''][] = $value; - } - - return new static($results); - } - - /** - * Get the "group by" key value. - * - * @param callable|string $groupBy - * @param string $key - * @param mixed $value - * @return string - */ - protected function getGroupByKey($groupBy, $key, $value) - { - if ( ! is_string($groupBy) && is_callable($groupBy)) - { - return $groupBy($value, $key); - } - - return data_get($value, $groupBy); - } - - /** - * Key an associative array by a field. - * - * @param string $keyBy - * @return static - */ - public function keyBy($keyBy) - { - $results = []; - - foreach ($this->items as $item) - { - $key = data_get($item, $keyBy); - - $results[$key] = $item; - } - - return new static($results); - } - - /** - * Determine if an item exists in the collection by key. - * - * @param mixed $key - * @return bool - */ - public function has($key) - { - return $this->offsetExists($key); - } - - /** - * Concatenate values of a given key as a string. - * - * @param string $value - * @param string $glue - * @return string - */ - public function implode($value, $glue = null) - { - $glue = $glue ?? ''; - - return implode($glue, $this->lists($value)); - } - - /** - * Intersect the collection with the given items. - * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return static - */ - public function intersect($items) - { - return new static(array_intersect($this->items, $this->getArrayableItems($items))); - } - - /** - * Determine if the collection is empty or not. - * - * @return bool - */ - public function isEmpty() - { - return empty($this->items); - } - - /** - * Get the keys of the collection items. - * - * @return array - */ - public function keys() - { - return array_keys($this->items); - } - - /** - * Get the last item from the collection. - * - * @return mixed|null - */ - public function last() - { - return count($this->items) > 0 ? end($this->items) : null; - } - - /** - * Get an array with the values of a given key. - * - * @param string $value - * @param ?string $key - * @return array - */ - public function lists(string $value, ?string $key = null): array - { - $results = array(); - - foreach ($this->items as $item) - { - $itemValue = is_object($item) ? $item->{$value} : $item[$value]; - - // If the key is "null", we will just append the value to the array and keep - // looping. Otherwise we will key the array using the value of the key we - // received from the developer. Then we'll return the final array form. - if (is_null($key)) { - $results[] = $itemValue; - } else { - $itemKey = is_object($item) ? $item->{$key} : $item[$key]; - - $results[$itemKey] = $itemValue; - } - } - - return $results; - } - - /** - * Run a map over each of the items. - * - * @param \Closure $callback - * @return static - */ - public function map(Closure $callback) - { - return new static(array_map($callback, $this->items, array_keys($this->items))); - } - - /** - * 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(TValue, TKey): array $callback - * @return static - */ - public function mapWithKeys(callable $callback) - { - $result = []; - - foreach ($this->items as $key => $value) { - $assoc = $callback($value, $key); - - foreach ($assoc as $mapKey => $mapValue) { - $result[$mapKey] = $mapValue; - } - } - - return new static($result); - } - - /** - * Merge the collection with the given items. - * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return static - */ - public function merge($items) - { - return new static(array_merge($this->items, $this->getArrayableItems($items))); - } - - /** - * Get and remove the last item from the collection. - * - * @return mixed|null - */ - public function pop() - { - return array_pop($this->items); - } - - /** - * Push an item onto the beginning of the collection. - * - * @param mixed $value - * @return void - */ - public function prepend($value) - { - array_unshift($this->items, $value); - } - - /** - * Push an item onto the end of the collection. - * - * @param mixed $value - * @return void - */ - public function push($value) - { - $this->items[] = $value; - } - - /** - * Pulls an item from the collection. - * - * @param mixed $key - * @param mixed $default - * @return mixed - */ - public function pull($key, $default = null) - { - return array_pull($this->items, $key, $default); - } - - /** - * Put an item in the collection by key. - * - * @param mixed $key - * @param mixed $value - * @return void - */ - public function put($key, $value) - { - $this->items[$key] = $value; - } - - /** - * Get one or more items randomly from the collection. - * - * @param int $amount - * @return mixed - */ - public function random($amount = 1) - { - if ($this->isEmpty()) return null; - - $keys = array_rand($this->items, $amount); - - return is_array($keys) ? array_intersect_key($this->items, array_flip($keys)) : $this->items[$keys]; - } - - /** - * Reduce the collection to a single value. - * - * @param callable $callback - * @param mixed $initial - * @return mixed - */ - public function reduce(callable $callback, $initial = null) - { - return array_reduce($this->items, $callback, $initial); - } - - /** - * Create a collection of all elements that do not pass a given truth test. - * - * @param \Closure|mixed $callback - * @return static - */ - public function reject($callback) - { - if ($callback instanceof Closure) - { - return $this->filter(function($item) use ($callback) - { - return ! $callback($item); - }); - } - - return $this->filter(function($item) use ($callback) - { - return $item != $callback; - }); - } - - /** - * Reverse items order. - * - * @return static - */ - public function reverse() - { - return new static(array_reverse($this->items)); - } - - /** - * Search the collection for a given value and return the corresponding key if successful. - * - * @param mixed $value - * @param bool $strict - * @return mixed - */ - public function search($value, $strict = false) - { - return array_search($value, $this->items, $strict); - } - - /** - * Get and remove the first item from the collection. - * - * @return mixed|null - */ - public function shift() - { - return array_shift($this->items); - } - - /** - * Shuffle the items in the collection. - * - * @return $this - */ - public function shuffle() - { - shuffle($this->items); - - return $this; - } - - /** - * Slice the underlying collection array. - * - * @param int $offset - * @param int $length - * @param bool $preserveKeys - * @return static - */ - public function slice($offset, $length = null, $preserveKeys = false) - { - return new static(array_slice($this->items, $offset, $length, $preserveKeys)); - } - - /** - * Chunk the underlying collection array. - * - * @param int $size - * @param bool $preserveKeys - * @return static - */ - public function chunk($size, $preserveKeys = false) - { - $chunks = new static; - - foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) - { - $chunks->push(new static($chunk)); - } - - return $chunks; - } - - /** - * Sort through each item with a callback. - * - * @param \Closure $callback - * @return $this - */ - public function sort(Closure $callback) - { - uasort($this->items, $callback); - - return $this; - } - - /** - * Sort the collection using the given Closure. - * - * @param \Closure|string $callback - * @param int $options - * @param bool $descending - * @return $this - */ - public function sortBy($callback, $options = SORT_REGULAR, $descending = false) - { - if (is_array($callback) && ! is_callable($callback)) { - return $this->sortByMany($callback); - } - - $results = []; - - $callback = $this->valueRetriever($callback); - - // First we will loop through the items and get the comparator from a callback - // function which we were given. Then, we will sort the returned values and - // and grab the corresponding values for the sorted keys from this array. - foreach ($this->items as $key => $value) - { - $results[$key] = $callback($value, $key); - } - - $descending ? arsort($results, $options) - : asort($results, $options); - - // Once we have sorted all of the keys in the array, we will loop through them - // and grab the corresponding model so we can set the underlying items list - // to the sorted version. Then we'll just return the collection instance. - foreach (array_keys($results) as $key) - { - $results[$key] = $this->items[$key]; - } - - $this->items = $results; - - return $this; - } - - /** - * Sort the collection in descending order using the given Closure. - * - * @param \Closure|string $callback - * @param int $options - * @return $this - */ - public function sortByDesc($callback, $options = SORT_REGULAR) - { - return $this->sortBy($callback, $options, true); - } - - /** - * Sort the collection using multiple comparisons. - * - * @param array $comparisons - * @return static - */ - protected function sortByMany(array $comparisons = []) - { - $items = $this->items; - - usort($items, static function ($a, $b) use ($comparisons) { - foreach ($comparisons as $comparison) { - $comparison = Arr::wrap($comparison); - - $prop = $comparison[0]; - - $ascending = Arr::get($comparison, 1, true) === true || - Arr::get($comparison, 1, true) === 'asc'; - - if (! is_string($prop) && is_callable($prop)) { - $result = $prop($a, $b); - } else { - $values = [data_get($a, $prop), data_get($b, $prop)]; - - if (! $ascending) { - $values = array_reverse($values); - } - - $result = $values[0] <=> $values[1]; - } - - if ($result === 0) { - continue; - } - - return $result; - } - }); - - return new static($items); - } - - /** - * Splice portion of the underlying collection array. - * - * @param int $offset - * @param int $length - * @param mixed $replacement - * @return static - */ - public function splice($offset, $length = 0, $replacement = array()) - { - return new static(array_splice($this->items, $offset, $length, $replacement)); - } - - /** - * Get the sum of the given values. - * - * @param \Closure $callback - * @return mixed - */ - public function sum($callback = null) - { - if (is_null($callback)) - { - return array_sum($this->items); - } - - if (is_string($callback)) - { - $callback = $this->valueRetriever($callback); - } - - return $this->reduce(function($result, $item) use ($callback) - { - return $result += $callback($item); - - }, 0); - } - - /** - * Take the first or last {$limit} items. - * - * @param int $limit - * @return static - */ - public function take($limit = null) - { - if ($limit < 0) return $this->slice($limit, abs($limit)); - - return $this->slice(0, $limit); - } - - /** - * Transform each item in the collection using a callback. - * - * @param \Closure $callback - * @return $this - */ - public function transform(Closure $callback) - { - $this->items = array_map($callback, $this->items); - - return $this; - } - - /** - * Return only unique items from the collection array. - * - * @return static - */ - public function unique() - { - return new static(array_unique($this->items)); - } - - /** - * Reset the keys on the underlying array. - * - * @return static - */ - public function values() - { - $this->items = array_values($this->items); - - return $this; - } - - /** - * Get a value retrieving callback. - * - * @param string $value - * @return \Closure - */ - protected function valueRetriever($value) - { - if ($this->useAsCallable($value)) { - return $value; - } - - return function ($item) use ($value) { - return data_get($item, $value); - }; - } - - /** - * Determine if the given value is callable, but not a string. - * - * @param mixed $value - * @return bool - */ - protected function useAsCallable($value) - { - return ! is_string($value) && is_callable($value); - } - - /** - * Get the collection of items as a plain array. - * - * @return array - */ - public function toArray() - { - return array_map(function($value) - { - return $value instanceof ArrayableInterface ? $value->toArray() : $value; - - }, $this->items); - } - - /** - * Convert the object into something JSON serializable. - */ - public function jsonSerialize(): mixed - { - return $this->toArray(); - } - - /** - * Get the collection of items as JSON. - * - * @param int $options - * @return string - */ - public function toJson($options = 0) - { - return json_encode($this->toArray(), $options); - } - - /** - * Get an iterator for the items. - */ - public function getIterator(): Traversable - { - return new ArrayIterator($this->items); - } - - /** - * Get a CachingIterator instance. - * - * @param int $flags - * @return \CachingIterator - */ - public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING) - { - return new CachingIterator($this->getIterator(), $flags); - } - - /** - * Count the number of items in the collection. - * - * @return int - */ - public function count(): int - { - return count($this->items); - } - - /** - * Determine if an item exists at an offset. - * - * @param mixed $key - * @return bool - */ - public function offsetExists($key): bool - { - return array_key_exists($key, $this->items); - } - - /** - * Get an item at a given offset. - * - * @param mixed $key - * @return mixed - */ - public function offsetGet($key): mixed - { - return $this->items[$key]; - } - - /** - * Set the item at a given offset. - * - * @param mixed $key - * @param mixed $value - * @return void - */ - public function offsetSet($key, $value): void - { - if (is_null($key)) - { - $this->items[] = $value; - } - else - { - $this->items[$key] = $value; - } - } - - /** - * Unset the item at a given offset. - * - * @param string $key - * @return void - */ - public function offsetUnset($key): void - { - unset($this->items[$key]); - } - - /** - * Convert the collection to its string representation. - * - * @return string - */ - public function __toString() - { - return $this->toJson(); - } - - /** - * Results array of items from Collection or ArrayableInterface. - * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return array - */ - protected function getArrayableItems($items) - { - if ($items instanceof Collection) - { - $items = $items->all(); - } - elseif ($items instanceof ArrayableInterface) - { - $items = $items->toArray(); - } - - return $items; - } - -} + + * @implements \Illuminate\Support\Enumerable + */ +class Collection implements ArrayAccess, ArrayableInterface, CanBeEscapedWhenCastToString, Enumerable, JsonableInterface +{ + /** + * @use \Illuminate\Support\Traits\EnumeratesValues + */ + use EnumeratesValues, Macroable; + + /** + * The items contained in the collection. + * + * @var array + */ + protected $items = []; + + /** + * Create a new collection. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + */ + public function __construct($items = []) + { + $this->items = $this->getArrayableItems($items); + } + + // ponytail: Laravel 4.2 holdovers kept so the fork's Database layer and existing + // callers keep working across the L13 shape change; removed in migration task 2.5 + // (lists → pluck, fetch → pluck) once app call-sites are converted. + + /** + * Get an array with the values of a given key. + * + * @param string $value + * @param string|null $key + * @return array + */ + public function lists(string $value, ?string $key = null): array + { + $results = []; + + foreach ($this->items as $item) { + $itemValue = is_object($item) ? $item->{$value} : $item[$value]; + + if (is_null($key)) { + $results[] = $itemValue; + } else { + $itemKey = is_object($item) ? $item->{$key} : $item[$key]; + + $results[$itemKey] = $itemValue; + } + } + + return $results; + } + + /** + * Fetch a nested element of the collection. + * + * @param string $key + * @return static + */ + public function fetch($key) + { + return new static(array_fetch($this->items, $key)); + } + + /** + * Create a new instance of the collection. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static + */ + protected function newInstance($items = []) + { + return new static($items); + } + + /** + * Create a collection with the given range. + * + * @param int $from + * @param int $to + * @param int $step + * @return static + */ + public static function range($from, $to, $step = 1, ...$args) + { + return new static(range($from, $to, $step), ...$args); + } + + /** + * Get all of the items in the collection. + * + * @return array + */ + public function all() + { + return $this->items; + } + + /** + * Get a lazy collection for the items in this collection. + * + * @return \Illuminate\Support\LazyCollection + */ + public function lazy() + { + return new LazyCollection($this->items); + } + + /** + * Get the median of a given key. + * + * @param string|array|null $key + * @return float|int|null + */ + public function median($key = null) + { + $values = (isset($key) ? $this->pluck($key) : $this) + ->reject(fn ($item) => is_null($item)) + ->sort()->values(); + + $count = $values->count(); + + if ($count === 0) { + return; + } + + $middle = intdiv($count, 2); + + if ($count % 2) { + return $values->get($middle); + } + + return $this->newInstance([ + $values->get($middle - 1), $values->get($middle), + ])->average(); + } + + /** + * Get the mode of a given key. + * + * @param string|array|null $key + * @return array|null + */ + public function mode($key = null) + { + if ($this->isEmpty()) { + return; + } + + $collection = isset($key) ? $this->pluck($key) : $this; + + $counts = $this->newInstance(); + + $collection->each(fn ($value) => $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1); + + $sorted = $counts->sort(); + + $highestValue = $sorted->last(); + + return $sorted->filter(fn ($value) => $value == $highestValue) + ->sort()->keys()->all(); + } + + /** + * Collapse the collection of items into a single array. + * + * @return static + */ + public function collapse() + { + return $this->newInstance(Arr::collapse($this->items)); + } + + /** + * Collapse the collection of items into a single array while preserving its keys. + * + * @return static + */ + public function collapseWithKeys() + { + if (! $this->items) { + return $this->newInstance(); + } + + $results = []; + + foreach ($this->items as $key => $values) { + if ($values instanceof Collection) { + $values = $values->all(); + } elseif (! is_array($values)) { + continue; + } + + $results[$key] = $values; + } + + if (! $results) { + return $this->newInstance(); + } + + return $this->newInstance(array_replace(...$results)); + } + + /** + * Determine if an item exists in the collection. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function contains($key, $operator = null, $value = null) + { + if (func_num_args() === 1) { + if ($this->useAsCallable($key)) { + return array_any($this->items, $key); + } + + return in_array($key, $this->items); + } + + return $this->contains($this->operatorForWhere(...func_get_args())); + } + + /** + * Determine if an item exists, using strict comparison. + * + * @param (callable(TValue): bool)|TValue|array-key $key + * @param TValue|null $value + * @return bool + */ + public function containsStrict($key, $value = null) + { + if (func_num_args() === 2) { + return $this->contains(fn ($item) => data_get($item, $key) === $value); + } + + if ($this->useAsCallable($key)) { + return ! is_null($this->first($key)); + } + + return in_array($key, $this->items, true); + } + + /** + * Determine if an item is not contained in the collection. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null) + { + return ! $this->contains(...func_get_args()); + } + + /** + * Determine if an item is not contained in the enumerable, using strict comparison. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContainStrict($key, $operator = null, $value = null) + { + return ! $this->containsStrict(...func_get_args()); + } + + /** + * Cross join with the given lists, returning all possible permutations. + * + * @template TCrossJoinKey + * @template TCrossJoinValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$lists + * @return static> + */ + public function crossJoin(...$lists) + { + return $this->newInstance(Arr::crossJoin( + $this->items, ...array_map($this->getArrayableItems(...), $lists) + )); + } + + /** + * Get the items in the collection that are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diff($items) + { + return $this->newInstance(array_diff($this->items, $this->getArrayableItems($items))); + } + + /** + * Get the items in the collection that are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function diffUsing($items, callable $callback) + { + return $this->newInstance(array_udiff($this->items, $this->getArrayableItems($items), $callback)); + } + + /** + * Get the items in the collection whose keys and values are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diffAssoc($items) + { + return $this->newInstance(array_diff_assoc($this->items, $this->getArrayableItems($items))); + } + + /** + * Get the items in the collection whose keys and values are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function diffAssocUsing($items, callable $callback) + { + return $this->newInstance(array_diff_uassoc($this->items, $this->getArrayableItems($items), $callback)); + } + + /** + * Get the items in the collection whose keys are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diffKeys($items) + { + return $this->newInstance(array_diff_key($this->items, $this->getArrayableItems($items))); + } + + /** + * Get the items in the collection whose keys are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function diffKeysUsing($items, callable $callback) + { + return $this->newInstance(array_diff_ukey($this->items, $this->getArrayableItems($items), $callback)); + } + + /** + * Retrieve duplicate items from the collection. + * + * @template TMapValue + * + * @param (callable(TValue): TMapValue)|string|null $callback + * @param bool $strict + * @return static + */ + public function duplicates($callback = null, $strict = false) + { + $items = $this->map($this->valueRetriever($callback)); + + $uniqueItems = $items->unique(null, $strict); + + $compare = $this->duplicateComparator($strict); + + $duplicates = $this->newInstance(); + + foreach ($items as $key => $value) { + if ($uniqueItems->isNotEmpty() && $compare($value, $uniqueItems->first())) { + $uniqueItems->shift(); + } else { + $duplicates[$key] = $value; + } + } + + return $duplicates; + } + + /** + * Retrieve duplicate items from the collection using strict comparison. + * + * @template TMapValue + * + * @param (callable(TValue): TMapValue)|string|null $callback + * @return static + */ + public function duplicatesStrict($callback = null) + { + return $this->duplicates($callback, true); + } + + /** + * Get the comparison function to detect duplicates. + * + * @param bool $strict + * @return callable(TValue, TValue): bool + */ + protected function duplicateComparator($strict) + { + if ($strict) { + return fn ($a, $b) => $a === $b; + } + + return fn ($a, $b) => $a == $b; + } + + /** + * Get all items except for those with the specified keys. + * + * @param \Illuminate\Support\Enumerable|array|string $keys + * @return static + */ + public function except($keys) + { + if (is_null($keys)) { + return $this->newInstance($this->items); + } + + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } elseif (! is_array($keys)) { + $keys = func_get_args(); + } + + return $this->newInstance(Arr::except($this->items, $keys)); + } + + /** + * Run a filter over each of the items. + * + * @param (callable(TValue, TKey): bool)|null $callback + * @return static + */ + public function filter(?callable $callback = null) + { + if ($callback) { + return $this->newInstance(Arr::where($this->items, $callback)); + } + + return $this->newInstance(array_filter($this->items)); + } + + /** + * Get the first item from the collection passing the given truth test. + * + * @template TFirstDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TFirstDefault|(\Closure(): TFirstDefault) $default + * @return TValue|TFirstDefault + */ + public function first(?callable $callback = null, $default = null) + { + return Arr::first($this->items, $callback, $default); + } + + /** + * Get a flattened array of the items in the collection. + * + * @param int $depth + * @return static + */ + public function flatten($depth = INF) + { + return $this->newInstance(Arr::flatten($this->items, $depth)); + } + + /** + * Flip the items in the collection. + * + * @return static + */ + public function flip() + { + return $this->newInstance(array_flip($this->items)); + } + + /** + * Remove an item from the collection by key. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|TKey $keys + * @return $this + */ + public function forget($keys) + { + foreach ($this->getArrayableItems($keys) as $key) { + $this->offsetUnset($key); + } + + return $this; + } + + /** + * Get an item from the collection by key. + * + * @template TGetDefault + * + * @param TKey|null $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TValue|TGetDefault + */ + public function get($key, $default = null) + { + $key ??= ''; + + if (array_key_exists($key, $this->items)) { + return $this->items[$key]; + } + + return value($default); + } + + /** + * Get an item from the collection by key or add it to collection if it does not exist. + * + * @template TGetOrPutValue + * + * @param mixed $key + * @param TGetOrPutValue|(\Closure(): TGetOrPutValue) $value + * @return TValue|TGetOrPutValue + */ + public function getOrPut($key, $value) + { + if (array_key_exists($key ?? '', $this->items)) { + return $this->items[$key ?? '']; + } + + $this->offsetSet($key, $value = value($value)); + + return $value; + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function groupBy($groupBy, $preserveKeys = false) + { + if (! $this->useAsCallable($groupBy) && is_array($groupBy)) { + $nextGroups = $groupBy; + + $groupBy = array_shift($nextGroups); + } + + $groupBy = $this->valueRetriever($groupBy); + + $results = []; + + foreach ($this->items as $key => $value) { + $groupKeys = $groupBy($value, $key); + + if (! is_array($groupKeys)) { + $groupKeys = [$groupKeys]; + } + + foreach ($groupKeys as $groupKey) { + $groupKey = match (true) { + is_bool($groupKey) => (int) $groupKey, + $groupKey instanceof \UnitEnum => enum_value($groupKey), + $groupKey instanceof \Stringable, is_null($groupKey) => (string) $groupKey, + default => $groupKey, + }; + + if (! array_key_exists($groupKey, $results)) { + $results[$groupKey] = $this->newInstance(); + } + + $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value); + } + } + + $result = $this->newInstance($results); + + if (! empty($nextGroups)) { + return $result->map->groupBy($nextGroups, $preserveKeys); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function keyBy($keyBy) + { + $keyBy = $this->valueRetriever($keyBy); + + $results = []; + + foreach ($this->items as $key => $item) { + $resolvedKey = $keyBy($item, $key); + + if ($resolvedKey instanceof \UnitEnum) { + $resolvedKey = enum_value($resolvedKey); + } + + if (is_object($resolvedKey)) { + $resolvedKey = (string) $resolvedKey; + } + + if (is_null($resolvedKey)) { + $resolvedKey = (string) $resolvedKey; + } + + $results[$resolvedKey] = $item; + } + + return $this->newInstance($results); + } + + /** + * Determine if an item exists in the collection by key. + * + * @param TKey|array $key + * @return bool + */ + public function has($key) + { + $keys = is_array($key) ? $key : func_get_args(); + + return array_all($keys, fn ($key) => array_key_exists($key ?? '', $this->items)); + } + + /** + * Determine if any of the keys exist in the collection. + * + * @param TKey|array $key + * @return bool + */ + public function hasAny($key) + { + if ($this->isEmpty()) { + return false; + } + + $keys = is_array($key) ? $key : func_get_args(); + + return array_any($keys, fn ($key) => array_key_exists($key ?? '', $this->items)); + } + + /** + * Concatenate values of a given key as a string. + * + * @param (callable(TValue, TKey): mixed)|string|null $value + * @param string|null $glue + * @return string + */ + public function implode($value, $glue = null) + { + if ($this->useAsCallable($value)) { + return implode($glue ?? '', $this->map($value)->all()); + } + + $first = $this->first(); + + if (is_array($first) || (is_object($first) && ! $first instanceof Stringable)) { + return implode($glue ?? '', $this->pluck($value)->all()); + } + + return implode($value ?? '', $this->items); + } + + /** + * Intersect the collection with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersect($items) + { + return $this->newInstance(array_intersect($this->items, $this->getArrayableItems($items))); + } + + /** + * Intersect the collection with the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function intersectUsing($items, callable $callback) + { + return $this->newInstance(array_uintersect($this->items, $this->getArrayableItems($items), $callback)); + } + + /** + * Intersect the collection with the given items with additional index check. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersectAssoc($items) + { + return $this->newInstance(array_intersect_assoc($this->items, $this->getArrayableItems($items))); + } + + /** + * Intersect the collection with the given items with additional index check, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function intersectAssocUsing($items, callable $callback) + { + return $this->newInstance(array_intersect_uassoc($this->items, $this->getArrayableItems($items), $callback)); + } + + /** + * Intersect the collection with the given items by key. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersectByKeys($items) + { + return $this->newInstance(array_intersect_key( + $this->items, $this->getArrayableItems($items) + )); + } + + /** + * Determine if the collection is empty or not. + * + * @phpstan-assert-if-true null $this->first() + * @phpstan-assert-if-true null $this->last() + * + * @phpstan-assert-if-false TValue $this->first() + * @phpstan-assert-if-false TValue $this->last() + * + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + /** + * Determine if the collection contains exactly one item. If a callback is provided, determine if exactly one item matches the condition. + * + * @param (callable(TValue, TKey): bool)|null $callback + * @return bool + * + * @deprecated 12.49.0 Use the `hasSole()` method instead. + */ + public function containsOneItem(?callable $callback = null): bool + { + return $this->hasSole($callback); + } + + /** + * Determine if the collection contains multiple items. + * + * @param (callable(TValue, TKey): bool)|null $callback + * @return bool + * + * @deprecated 12.50.0 Use the `hasMany()` method instead. + */ + public function containsManyItems(?callable $callback = null): bool + { + return $this->hasMany($callback); + } + + /** + * Join all items from the collection using a string. The final items can use a separate glue string. + * + * @param string $glue + * @param string $finalGlue + * @return TValue|string + */ + public function join($glue, $finalGlue = '') + { + if ($finalGlue === '') { + return $this->implode($glue); + } + + $count = $this->count(); + + if ($count === 0) { + return ''; + } + + if ($count === 1) { + return $this->last(); + } + + $collection = $this->newInstance($this->items); + + $finalItem = $collection->pop(); + + return $collection->implode($glue).$finalGlue.$finalItem; + } + + /** + * Get the keys of the collection items. + * + * @return static + */ + public function keys() + { + return $this->newInstance(array_keys($this->items)); + } + + /** + * Get the last item from the collection. + * + * @template TLastDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TLastDefault|(\Closure(): TLastDefault) $default + * @return TValue|TLastDefault + */ + public function last(?callable $callback = null, $default = null) + { + return Arr::last($this->items, $callback, $default); + } + + /** + * Get the values of a given key. + * + * @param \Closure|string|int|array|null $value + * @param \Closure|string|null $key + * @return static + */ + public function pluck($value, $key = null) + { + return $this->newInstance(Arr::pluck($this->items, $value, $key)); + } + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return static + */ + public function map(callable $callback) + { + return $this->newInstance(Arr::map($this->items, $callback)); + } + + /** + * Run a dictionary map over the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapToDictionaryKey of array-key + * @template TMapToDictionaryValue + * + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToDictionary(callable $callback) + { + $dictionary = []; + + foreach ($this->items as $key => $item) { + $pair = $callback($item, $key); + + $key = key($pair); + + $value = reset($pair); + + if (! isset($dictionary[$key])) { + $dictionary[$key] = []; + } + + $dictionary[$key][] = $value; + } + + return $this->newInstance($dictionary); + } + + /** + * 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(TValue, TKey): array $callback + * @return static + */ + public function mapWithKeys(callable $callback) + { + return $this->newInstance(Arr::mapWithKeys($this->items, $callback)); + } + + /** + * Merge the collection with the given items. + * + * @template TMergeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function merge($items) + { + return $this->newInstance(array_merge($this->items, $this->getArrayableItems($items))); + } + + /** + * Recursively merge the collection with the given items. + * + * @template TMergeRecursiveValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function mergeRecursive($items) + { + return $this->newInstance(array_merge_recursive($this->items, $this->getArrayableItems($items))); + } + + /** + * Multiply the items in the collection by the multiplier. + * + * @param int $multiplier + * @return static + */ + public function multiply(int $multiplier) + { + $new = $this->newInstance(); + + for ($i = 0; $i < $multiplier; $i++) { + $new->push(...$this->items); + } + + return $new; + } + + /** + * Create a collection by using this collection for keys and another for its values. + * + * @template TCombineValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function combine($values) + { + return $this->newInstance(array_combine($this->all(), $this->getArrayableItems($values))); + } + + /** + * Union the collection with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function union($items) + { + return $this->newInstance($this->items + $this->getArrayableItems($items)); + } + + /** + * Create a new collection consisting of every n-th element. + * + * @param int $step + * @param int $offset + * @return ($step is positive-int ? static : never) + * + * @throws \InvalidArgumentException + */ + public function nth($step, $offset = 0) + { + if ($step < 1) { + throw new InvalidArgumentException('Step value must be at least 1.'); + } + + $new = []; + + $position = 0; + + foreach ($this->slice($offset)->items as $item) { + if ($position % $step === 0) { + $new[] = $item; + } + + $position++; + } + + return $this->newInstance($new); + } + + /** + * Get the items with the specified keys. + * + * @param \Illuminate\Support\Enumerable|array|string|null $keys + * @return static + */ + public function only($keys) + { + if (is_null($keys)) { + return $this->newInstance($this->items); + } + + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } + + $keys = is_array($keys) ? $keys : func_get_args(); + + return $this->newInstance(Arr::only($this->items, $keys)); + } + + /** + * Select specific values from the items within the collection. + * + * @param \Illuminate\Support\Enumerable|list|string|null $keys + * @return static + */ + public function select($keys) + { + if (is_null($keys)) { + return $this->newInstance($this->items); + } + + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } + + $keys = is_array($keys) ? $keys : func_get_args(); + + return $this->newInstance(Arr::select($this->items, $keys)); + } + + /** + * Get and remove the last N items from the collection. + * + * @param int $count + * @return ($count is 1 ? TValue|null : static) + */ + public function pop($count = 1) + { + if ($count < 1) { + return $this->newInstance(); + } + + if ($count === 1) { + return array_pop($this->items); + } + + if ($this->isEmpty()) { + return $this->newInstance(); + } + + $results = []; + + $collectionCount = $this->count(); + + foreach (range(1, min($count, $collectionCount)) as $item) { + $results[] = array_pop($this->items); + } + + return $this->newInstance($results); + } + + /** + * Push an item onto the beginning of the collection. + * + * @param TValue $value + * @param TKey $key + * @return $this + */ + public function prepend($value, $key = null) + { + $this->items = Arr::prepend($this->items, ...(func_num_args() > 1 ? func_get_args() : [$value])); + + return $this; + } + + /** + * Push one or more items onto the end of the collection. + * + * @param TValue ...$values + * @return $this + */ + public function push(...$values) + { + foreach ($values as $value) { + $this->items[] = $value; + } + + return $this; + } + + /** + * Prepend one or more items to the beginning of the collection. + * + * @param TValue ...$values + * @return $this + */ + public function unshift(...$values) + { + array_unshift($this->items, ...$values); + + return $this; + } + + /** + * Push all of the given items onto the collection. + * + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static + */ + public function concat($source) + { + $result = $this->newInstance($this); + + foreach ($source as $item) { + $result->push($item); + } + + return $result; + } + + /** + * Get and remove an item from the collection. + * + * @template TPullDefault + * + * @param TKey $key + * @param TPullDefault|(\Closure(): TPullDefault) $default + * @return TValue|TPullDefault + */ + public function pull($key, $default = null) + { + return Arr::pull($this->items, $key, $default); + } + + /** + * Put an item in the collection by key. + * + * @param TKey $key + * @param TValue $value + * @return $this + */ + public function put($key, $value) + { + $this->offsetSet($key, $value); + + return $this; + } + + /** + * Get one or a specified number of items randomly from the collection. + * + * @param (callable(self): int)|int|null $number + * @param bool $preserveKeys + * @return ($number is null ? TValue : static) + * + * @throws \InvalidArgumentException + */ + public function random($number = null, $preserveKeys = false) + { + if (is_null($number)) { + return Arr::random($this->items); + } + + if (is_callable($number)) { + return $this->newInstance(Arr::random($this->items, $number($this), $preserveKeys)); + } + + return $this->newInstance(Arr::random($this->items, $number, $preserveKeys)); + } + + /** + * Replace the collection items with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function replace($items) + { + return $this->newInstance(array_replace($this->items, $this->getArrayableItems($items))); + } + + /** + * Recursively replace the collection items with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function replaceRecursive($items) + { + return $this->newInstance(array_replace_recursive($this->items, $this->getArrayableItems($items))); + } + + /** + * Reverse items order. + * + * @return static + */ + public function reverse() + { + return $this->newInstance(array_reverse($this->items, true)); + } + + /** + * Search the collection for a given value and return the corresponding key if successful. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TKey|false + */ + public function search($value, $strict = false) + { + if (! $this->useAsCallable($value)) { + return array_search($value, $this->items, $strict); + } + + return array_find_key($this->items, $value) ?? false; + } + + /** + * Get the item before the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function before($value, $strict = false) + { + $key = $this->search($value, $strict); + + if ($key === false) { + return null; + } + + $position = ($keys = $this->keys())->search($key); + + if ($position === 0) { + return null; + } + + return $this->get($keys->get($position - 1)); + } + + /** + * Get the item after the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function after($value, $strict = false) + { + $key = $this->search($value, $strict); + + if ($key === false) { + return null; + } + + $position = ($keys = $this->keys())->search($key); + + if ($position === $keys->count() - 1) { + return null; + } + + return $this->get($keys->get($position + 1)); + } + + /** + * Get and remove the first N items from the collection. + * + * @param int<0, max> $count + * @return ($count is 1 ? TValue|null : static) + * + * @throws \InvalidArgumentException + */ + public function shift($count = 1) + { + if ($count < 0) { + throw new InvalidArgumentException('Number of shifted items may not be less than zero.'); + } + + if ($this->isEmpty()) { + return null; + } + + if ($count === 0) { + return $this->newInstance(); + } + + if ($count === 1) { + return array_shift($this->items); + } + + $results = []; + + $collectionCount = $this->count(); + + foreach (range(1, min($count, $collectionCount)) as $item) { + $results[] = array_shift($this->items); + } + + return $this->newInstance($results); + } + + /** + * Shuffle the items in the collection. + * + * @return static + */ + public function shuffle() + { + return $this->newInstance(Arr::shuffle($this->items)); + } + + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param positive-int $size + * @param positive-int $step + * @return static + * + * @throws \InvalidArgumentException + */ + public function sliding($size = 2, $step = 1) + { + if ($size < 1) { + throw new InvalidArgumentException('Size value must be at least 1.'); + } elseif ($step < 1) { + throw new InvalidArgumentException('Step value must be at least 1.'); + } + + $chunks = floor(($this->count() - $size) / $step) + 1; + + return static::times($chunks, fn ($number) => $this->slice(($number - 1) * $step, $size)); + } + + /** + * Skip the first {$count} items. + * + * @param int $count + * @return static + */ + public function skip($count) + { + return $this->slice($count); + } + + /** + * Skip items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipUntil($value) + { + return $this->newInstance($this->lazy()->skipUntil($value)->all()); + } + + /** + * Skip items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipWhile($value) + { + return $this->newInstance($this->lazy()->skipWhile($value)->all()); + } + + /** + * Slice the underlying collection array. + * + * @param int $offset + * @param int|null $length + * @return static + */ + public function slice($offset, $length = null) + { + return $this->newInstance(array_slice($this->items, $offset, $length, true)); + } + + /** + * Split a collection into a certain number of groups. + * + * @param int $numberOfGroups + * @return ($numberOfGroups is positive-int ? static : never) + * + * @throws \InvalidArgumentException + */ + public function split($numberOfGroups) + { + if ($numberOfGroups < 1) { + throw new InvalidArgumentException('Number of groups must be at least 1.'); + } + + if ($this->isEmpty()) { + return $this->newInstance(); + } + + $groups = $this->newInstance(); + + $groupSize = floor($this->count() / $numberOfGroups); + + $remain = $this->count() % $numberOfGroups; + + $start = 0; + + for ($i = 0; $i < $numberOfGroups; $i++) { + $size = $groupSize; + + if ($i < $remain) { + $size++; + } + + if ($size) { + $groups->push($this->newInstance(array_slice($this->items, $start, $size))); + + $start += $size; + } + } + + return $groups; + } + + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return ($numberOfGroups is positive-int ? static : never) + * + * @throws \InvalidArgumentException + */ + public function splitIn($numberOfGroups) + { + if ($numberOfGroups < 1) { + throw new InvalidArgumentException('Number of groups must be at least 1.'); + } + + return $this->chunk((int) ceil($this->count() / $numberOfGroups)); + } + + /** + * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + * @throws \Illuminate\Support\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + $items = $this->unless($filter == null)->filter($filter); + + $count = $items->count(); + + if ($count === 0) { + throw new ItemNotFoundException; + } + + if ($count > 1) { + throw new MultipleItemsFoundException($count); + } + + return $items->first(); + } + + /** + * Determine if the collection contains a single item, optionally matching the given criteria. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function hasSole($key = null, $operator = null, $value = null): bool + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->unless($filter == null) + ->filter($filter) + ->count() === 1; + } + + /** + * Get the first item in the collection but throw an exception if no matching items exist. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + */ + public function firstOrFail($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + $placeholder = new stdClass(); + + $item = $this->first($filter, $placeholder); + + if ($item === $placeholder) { + throw new ItemNotFoundException; + } + + return $item; + } + + /** + * Chunk the collection into chunks of the given size. + * + * @param int $size + * @param bool $preserveKeys + * @return ($preserveKeys is true ? static : static>) + */ + public function chunk($size, $preserveKeys = true) + { + if ($size <= 0) { + return $this->newInstance(); + } + + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = $this->newInstance($chunk); + } + + return $this->newInstance($chunks); + } + + /** + * Chunk the collection into chunks with a callback. + * + * @param callable(TValue, TKey, static): bool $callback + * @return static> + */ + public function chunkWhile(callable $callback) + { + return $this->newInstance( + $this->lazy()->chunkWhile($callback)->mapInto(static::class) + ); + } + + /** + * Sort through each item with a callback. + * + * @param (callable(TValue, TValue): int)|null|int $callback + * @return static + */ + public function sort($callback = null) + { + $items = $this->items; + + $callback && is_callable($callback) + ? uasort($items, $callback) + : asort($items, $callback ?? SORT_REGULAR); + + return $this->newInstance($items); + } + + /** + * Sort items in descending order. + * + * @param int-mask-of $options + * @return static + */ + public function sortDesc($options = SORT_REGULAR) + { + $items = $this->items; + + arsort($items, $options); + + return $this->newInstance($items); + } + + /** + * Sort the collection using the given callback. + * + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options + * @param SortDirection|bool $descending + * @return static + */ + public function sortBy($callback, $options = SORT_REGULAR, $descending = false) + { + if (is_array($callback) && ! is_callable($callback)) { + return $this->sortByMany($callback, $options); + } + + $results = []; + + $callback = $this->valueRetriever($callback); + + // First we will loop through the items and get the comparator from a callback + // function which we were given. Then, we will sort the returned values and + // grab all the corresponding values for the sorted keys from this array. + foreach ($this->items as $key => $value) { + $results[$key] = $callback($value, $key); + } + + match ($descending) { + false, SortDirection::Ascending => asort($results, $options), + true, SortDirection::Descending => arsort($results, $options), + }; + + // Once we have sorted all of the keys in the array, we will loop through them + // and grab the corresponding model so we can set the underlying items list + // to the sorted version. Then we'll just return the collection instance. + foreach (array_keys($results) as $key) { + $results[$key] = $this->items[$key]; + } + + return $this->newInstance($results); + } + + /** + * Sort the collection using multiple comparisons. + * + * @param array $comparisons + * @param int-mask-of $options + * @return static + */ + protected function sortByMany(array $comparisons = [], int $options = SORT_REGULAR) + { + $items = $this->items; + + uasort($items, function ($a, $b) use ($comparisons, $options) { + foreach ($comparisons as $comparison) { + $comparison = Arr::wrap($comparison); + + $prop = $comparison[0]; + + $direction = match (Arr::get($comparison, 1, true)) { + true, 'asc', SortDirection::Ascending => SortDirection::Ascending, + false, 'desc', SortDirection::Descending => SortDirection::Descending, + default => SortDirection::Descending, // for backwards compatibility + }; + + if (! is_string($prop) && is_callable($prop)) { + $result = $prop($a, $b); + } else { + $values = [data_get($a, $prop), data_get($b, $prop)]; + + if ($direction === SortDirection::Descending) { + $values = array_reverse($values); + } + + if (($options & SORT_FLAG_CASE) === SORT_FLAG_CASE) { + if (($options & SORT_NATURAL) === SORT_NATURAL) { + $result = strnatcasecmp($values[0], $values[1]); + } else { + $result = strcasecmp($values[0], $values[1]); + } + } else { + $result = match ($options) { + SORT_NUMERIC => (int) $values[0] <=> (int) $values[1], + SORT_STRING => strcmp($values[0], $values[1]), + SORT_NATURAL => strnatcmp((string) $values[0], (string) $values[1]), + SORT_LOCALE_STRING => strcoll($values[0], $values[1]), + default => $values[0] <=> $values[1], + }; + } + } + + if ($result === 0) { + continue; + } + + return $result; + } + }); + + return $this->newInstance($items); + } + + /** + * Sort the collection in descending order using the given callback. + * + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options + * @return static + */ + public function sortByDesc($callback, $options = SORT_REGULAR) + { + if (is_array($callback) && ! is_callable($callback)) { + foreach ($callback as $index => $key) { + $comparison = Arr::wrap($key); + + $comparison[1] = SortDirection::Descending; + + $callback[$index] = $comparison; + } + } + + return $this->sortBy($callback, $options, true); + } + + /** + * Sort the collection keys. + * + * @param int-mask-of $options + * @param SortDirection|bool $descending + * @return static + */ + public function sortKeys($options = SORT_REGULAR, $descending = false) + { + $items = $this->items; + + match ($descending) { + false, SortDirection::Ascending => ksort($items, $options), + true, SortDirection::Descending => krsort($items, $options), + }; + + return $this->newInstance($items); + } + + /** + * Sort the collection keys in descending order. + * + * @param int-mask-of $options + * @return static + */ + public function sortKeysDesc($options = SORT_REGULAR) + { + return $this->sortKeys($options, SortDirection::Descending); + } + + /** + * Sort the collection keys using a callback. + * + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function sortKeysUsing(callable $callback) + { + $items = $this->items; + + uksort($items, $callback); + + return $this->newInstance($items); + } + + /** + * Splice a portion of the underlying collection array. + * + * @param int $offset + * @param int|null $length + * @param array $replacement + * @return static + */ + public function splice($offset, $length = null, $replacement = []) + { + if (func_num_args() === 1) { + return $this->newInstance(array_splice($this->items, $offset)); + } + + return $this->newInstance(array_splice($this->items, $offset, $length, $this->getArrayableItems($replacement))); + } + + /** + * Take the first or last {$limit} items. + * + * @param int $limit + * @return static + */ + public function take($limit) + { + if ($limit < 0) { + return $this->slice($limit, abs($limit)); + } + + return $this->slice(0, $limit); + } + + /** + * Take items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeUntil($value) + { + return $this->newInstance($this->lazy()->takeUntil($value)->all()); + } + + /** + * Take items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeWhile($value) + { + return $this->newInstance($this->lazy()->takeWhile($value)->all()); + } + + /** + * Transform each item in the collection using a callback. + * + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return $this + * + * @phpstan-this-out static + */ + public function transform(callable $callback) + { + $this->items = $this->map($callback)->all(); + + return $this; + } + + /** + * Flatten a multi-dimensional associative array with dots. + * + * @param int $depth + * @return static + */ + public function dot($depth = INF) + { + return $this->newInstance(Arr::dot($this->all(), '', $depth)); + } + + /** + * Convert a flatten "dot" notation array into an expanded array. + * + * @return static + */ + public function undot() + { + return $this->newInstance(Arr::undot($this->all())); + } + + /** + * Return only unique items from the collection array. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @param bool $strict + * @return static + */ + public function unique($key = null, $strict = false) + { + if (is_null($key) && $strict === false) { + return $this->newInstance(array_unique($this->items, SORT_REGULAR)); + } + + $callback = $this->valueRetriever($key); + + $exists = []; + + return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) { + if (in_array($id = $callback($item, $key), $exists, $strict)) { + return true; + } + + $exists[] = $id; + }); + } + + /** + * Reset the keys on the underlying array. + * + * @return static + */ + public function values() + { + return $this->newInstance(array_values($this->items)); + } + + /** + * Zip the collection together with one or more arrays. + * + * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]); + * => [[1, 4], [2, 5], [3, 6]] + * + * @template TZipValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$items + * @return static> + */ + public function zip($items) + { + $arrayableItems = array_map(fn ($items) => $this->getArrayableItems($items), func_get_args()); + + $params = array_merge([fn () => $this->newInstance(func_get_args()), $this->items], $arrayableItems); + + return $this->newInstance(array_map(...$params)); + } + + /** + * Pad collection to the specified length with a value. + * + * @template TPadValue + * + * @param int $size + * @param TPadValue $value + * @return static + */ + public function pad($size, $value) + { + return $this->newInstance(array_pad($this->items, $size, $value)); + } + + /** + * Get an iterator for the items. + * + * @return \ArrayIterator + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + /** + * Count the number of items in the collection. + * + * @return int<0, max> + */ + public function count(): int + { + return count($this->items); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function countBy($countBy = null) + { + return $this->newInstance($this->lazy()->countBy($countBy)->all()); + } + + /** + * Add an item to the collection. + * + * @param TValue $item + * @return $this + */ + public function add($item) + { + $this->items[] = $item; + + return $this; + } + + /** + * Get a base Support collection instance from this collection. + * + * @return \Illuminate\Support\Collection + */ + public function toBase() + { + return new self($this); + } + + /** + * Determine if an item exists at an offset. + * + * @param TKey $offset + * @return bool + */ + public function offsetExists($offset): bool + { + return isset($this->items[$offset]); + } + + /** + * Get an item at a given offset. + * + * @param TKey $offset + * @return TValue + */ + public function offsetGet($offset): mixed + { + return $this->items[$offset]; + } + + /** + * Set the item at a given offset. + * + * @param TKey|null $offset + * @param TValue $value + * @return void + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** + * Unset the item at a given offset. + * + * @param TKey $offset + * @return void + */ + public function offsetUnset($offset): void + { + unset($this->items[$offset]); + } +} diff --git a/src/Illuminate/Support/Enumerable.php b/src/Illuminate/Support/Enumerable.php new file mode 100644 index 00000000..36e04705 --- /dev/null +++ b/src/Illuminate/Support/Enumerable.php @@ -0,0 +1,1379 @@ + + * @extends \IteratorAggregate + */ +interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable +{ + /** + * Create a new collection instance if the value isn't one already. + * + * @template TMakeKey of array-key + * @template TMakeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static + */ + public static function make($items = []); + + /** + * Create a new instance by invoking the callback a given amount of times. + * + * @template TTimesValue + * + * @param int $number + * @param (callable(int): TTimesValue)|null $callback + * @return static + */ + public static function times($number, ?callable $callback = null); + + /** + * Create a collection with the given range. + * + * @param int $from + * @param int $to + * @param int $step + * @return static + */ + public static function range($from, $to, $step = 1); + + /** + * Wrap the given value in a collection if applicable. + * + * @template TWrapValue + * + * @param iterable|TWrapValue $value + * @return static + */ + public static function wrap($value); + + /** + * Get the underlying items from the given collection if applicable. + * + * @template TUnwrapKey of array-key + * @template TUnwrapValue + * + * @param array|static $value + * @return array + */ + public static function unwrap($value); + + /** + * Create a new instance with no items. + * + * @return static + */ + public static function empty(); + + /** + * Get all items in the enumerable. + * + * @return array + */ + public function all(); + + /** + * Alias for the "avg" method. + * + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null + */ + public function average($callback = null); + + /** + * Get the median of a given key. + * + * @param string|array|null $key + * @return float|int|null + */ + public function median($key = null); + + /** + * Get the mode of a given key. + * + * @param string|array|null $key + * @return array|null + */ + public function mode($key = null); + + /** + * Collapse the items into a single enumerable. + * + * @return static + */ + public function collapse(); + + /** + * Alias for the "contains" method. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function some($key, $operator = null, $value = null); + + /** + * Determine if an item exists, using strict comparison. + * + * @param (callable(TValue): bool)|TValue|array-key $key + * @param TValue|null $value + * @return bool + */ + public function containsStrict($key, $value = null); + + /** + * Get the average value of a given key. + * + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null + */ + public function avg($callback = null); + + /** + * Determine if an item exists in the enumerable. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function contains($key, $operator = null, $value = null); + + /** + * Determine if an item is not contained in the collection. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null); + + /** + * Cross join with the given lists, returning all possible permutations. + * + * @template TCrossJoinKey + * @template TCrossJoinValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$lists + * @return static> + */ + public function crossJoin(...$lists); + + /** + * Dump the collection and end the script. + * + * @param mixed ...$args + * @return never + */ + public function dd(...$args); + + /** + * Dump the collection. + * + * @param mixed ...$args + * @return $this + */ + public function dump(...$args); + + /** + * Get the items that are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diff($items); + + /** + * Get the items that are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function diffUsing($items, callable $callback); + + /** + * Get the items whose keys and values are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diffAssoc($items); + + /** + * Get the items whose keys and values are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function diffAssocUsing($items, callable $callback); + + /** + * Get the items whose keys are not present in the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function diffKeys($items); + + /** + * Get the items whose keys are not present in the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function diffKeysUsing($items, callable $callback); + + /** + * Retrieve duplicate items. + * + * @param (callable(TValue): bool)|string|null $callback + * @param bool $strict + * @return static + */ + public function duplicates($callback = null, $strict = false); + + /** + * Retrieve duplicate items using strict comparison. + * + * @param (callable(TValue): bool)|string|null $callback + * @return static + */ + public function duplicatesStrict($callback = null); + + /** + * Execute a callback over each item. + * + * @param callable(TValue, TKey): mixed $callback + * @return $this + */ + public function each(callable $callback); + + /** + * Execute a callback over each nested chunk of items. + * + * @param callable $callback + * @return static + */ + public function eachSpread(callable $callback); + + /** + * Determine if all items pass the given truth test. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function every($key, $operator = null, $value = null); + + /** + * Get all items except for those with the specified keys. + * + * @param \Illuminate\Support\Enumerable|array $keys + * @return static + */ + public function except($keys); + + /** + * Run a filter over each of the items. + * + * @param (callable(TValue): bool)|null $callback + * @return static + */ + public function filter(?callable $callback = null); + + /** + * Apply the callback if the given "value" is (or resolves to) truthy. + * + * @template TWhenReturnType as null + * + * @param bool $value + * @param (callable($this): TWhenReturnType)|null $callback + * @param (callable($this): TWhenReturnType)|null $default + * @return $this|TWhenReturnType + */ + public function when($value, ?callable $callback = null, ?callable $default = null); + + /** + * Apply the callback if the collection is empty. + * + * @template TWhenEmptyReturnType + * + * @param (callable($this): TWhenEmptyReturnType) $callback + * @param (callable($this): TWhenEmptyReturnType)|null $default + * @return $this|TWhenEmptyReturnType + */ + public function whenEmpty(callable $callback, ?callable $default = null); + + /** + * Apply the callback if the collection is not empty. + * + * @template TWhenNotEmptyReturnType + * + * @param callable($this): TWhenNotEmptyReturnType $callback + * @param (callable($this): TWhenNotEmptyReturnType)|null $default + * @return $this|TWhenNotEmptyReturnType + */ + public function whenNotEmpty(callable $callback, ?callable $default = null); + + /** + * Apply the callback if the given "value" is (or resolves to) falsy. + * + * @template TUnlessReturnType + * + * @param bool $value + * @param (callable($this): TUnlessReturnType) $callback + * @param (callable($this): TUnlessReturnType)|null $default + * @return $this|TUnlessReturnType + */ + public function unless($value, callable $callback, ?callable $default = null); + + /** + * Apply the callback unless the collection is empty. + * + * @template TUnlessEmptyReturnType + * + * @param callable($this): TUnlessEmptyReturnType $callback + * @param (callable($this): TUnlessEmptyReturnType)|null $default + * @return $this|TUnlessEmptyReturnType + */ + public function unlessEmpty(callable $callback, ?callable $default = null); + + /** + * Apply the callback unless the collection is not empty. + * + * @template TUnlessNotEmptyReturnType + * + * @param callable($this): TUnlessNotEmptyReturnType $callback + * @param (callable($this): TUnlessNotEmptyReturnType)|null $default + * @return $this|TUnlessNotEmptyReturnType + */ + public function unlessNotEmpty(callable $callback, ?callable $default = null); + + /** + * Filter items by the given key value pair. + * + * @param string $key + * @param mixed $operator + * @param mixed $value + * @return static + */ + public function where($key, $operator = null, $value = null); + + /** + * Filter items where the value for the given key is null. + * + * @param string|null $key + * @return static + */ + public function whereNull($key = null); + + /** + * Filter items where the value for the given key is not null. + * + * @param string|null $key + * @return static + */ + public function whereNotNull($key = null); + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param mixed $value + * @return static + */ + public function whereStrict($key, $value); + + /** + * Filter items by the given key value pair. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @param bool $strict + * @return static + */ + public function whereIn($key, $values, $strict = false); + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereInStrict($key, $values); + + /** + * Filter items such that the value of the given key is between the given values. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereBetween($key, $values); + + /** + * Filter items such that the value of the given key is not between the given values. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereNotBetween($key, $values); + + /** + * Filter items by the given key value pair. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @param bool $strict + * @return static + */ + public function whereNotIn($key, $values, $strict = false); + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereNotInStrict($key, $values); + + /** + * Filter the items, removing any items that don't match the given type(s). + * + * @template TWhereInstanceOf + * + * @param class-string|array> $type + * @return static + */ + public function whereInstanceOf($type); + + /** + * Get the first item from the enumerable passing the given truth test. + * + * @template TFirstDefault + * + * @param (callable(TValue,TKey): bool)|null $callback + * @param TFirstDefault|(\Closure(): TFirstDefault) $default + * @return TValue|TFirstDefault + */ + public function first(?callable $callback = null, $default = null); + + /** + * Get the first item by the given key value pair. + * + * @param string $key + * @param mixed $operator + * @param mixed $value + * @return TValue|null + */ + public function firstWhere($key, $operator = null, $value = null); + + /** + * Get a flattened array of the items in the collection. + * + * @param int $depth + * @return static + */ + public function flatten($depth = INF); + + /** + * Flip the values with their keys. + * + * @return static + */ + public function flip(); + + /** + * Get an item from the collection by key. + * + * @template TGetDefault + * + * @param TKey $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TValue|TGetDefault + */ + public function get($key, $default = null); + + /** + * Group an associative array by a field or using a callback. + * + * @template TGroupKey of array-key|\UnitEnum|\Stringable + * + * @param (callable(TValue, TKey): TGroupKey)|array|string $groupBy + * @param bool $preserveKeys + * @return static< + * ($groupBy is (array|string) + * ? array-key + * : (TGroupKey is \UnitEnum ? array-key : (TGroupKey is \Stringable ? string : TGroupKey))), + * static<($preserveKeys is true ? TKey : int), ($groupBy is array ? mixed : TValue)> + * > + */ + public function groupBy($groupBy, $preserveKeys = false); + + /** + * Key an associative array by a field or using a callback. + * + * @template TNewKey of array-key|\UnitEnum + * + * @param (callable(TValue, TKey): TNewKey)|array|string $keyBy + * @return static<($keyBy is (array|string) ? array-key : (TNewKey is \UnitEnum ? array-key : TNewKey)), TValue> + */ + public function keyBy($keyBy); + + /** + * Determine if an item exists in the collection by key. + * + * @param TKey|array $key + * @return bool + */ + public function has($key); + + /** + * Determine if any of the keys exist in the collection. + * + * @param mixed $key + * @return bool + */ + public function hasAny($key); + + /** + * Concatenate values of a given key as a string. + * + * @param (callable(TValue, TKey): mixed)|string $value + * @param string|null $glue + * @return string + */ + public function implode($value, $glue = null); + + /** + * Intersect the collection with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersect($items); + + /** + * Intersect the collection with the given items, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function intersectUsing($items, callable $callback); + + /** + * Intersect the collection with the given items with additional index check. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersectAssoc($items); + + /** + * Intersect the collection with the given items with additional index check, using the callback. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback + * @return static + */ + public function intersectAssocUsing($items, callable $callback); + + /** + * Intersect the collection with the given items by key. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function intersectByKeys($items); + + /** + * Determine if the collection is empty or not. + * + * @return bool + */ + public function isEmpty(); + + /** + * Determine if the collection is not empty. + * + * @return bool + */ + public function isNotEmpty(); + + /** + * Determine if the collection contains a single item. + * + * @return bool + */ + public function containsOneItem(); + + /** + * Determine if the collection contains multiple items. + * + * @return bool + */ + public function containsManyItems(); + + /** + * Determine if the collection contains a single item, optionally matching the given criteria. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function hasSole($key = null, $operator = null, $value = null); + + /** + * Determine if the collection contains multiple items, optionally matching the given criteria. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function hasMany($key = null, $operator = null, $value = null); + + /** + * Join all items from the collection using a string. The final items can use a separate glue string. + * + * @param string $glue + * @param string $finalGlue + * @return string + */ + public function join($glue, $finalGlue = ''); + + /** + * Get the keys of the collection items. + * + * @return static + */ + public function keys(); + + /** + * Get the last item from the collection. + * + * @template TLastDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TLastDefault|(\Closure(): TLastDefault) $default + * @return TValue|TLastDefault + */ + public function last(?callable $callback = null, $default = null); + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return static + */ + public function map(callable $callback); + + /** + * Run a map over each nested chunk of items. + * + * @param callable $callback + * @return static + */ + public function mapSpread(callable $callback); + + /** + * Run a dictionary map over the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapToDictionaryKey of array-key + * @template TMapToDictionaryValue + * + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToDictionary(callable $callback); + + /** + * Run a grouping map over the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapToGroupsKey of array-key + * @template TMapToGroupsValue + * + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToGroups(callable $callback); + + /** + * 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(TValue, TKey): array $callback + * @return static + */ + public function mapWithKeys(callable $callback); + + /** + * Map a collection and flatten the result by a single level. + * + * @template TFlatMapKey of array-key + * @template TFlatMapValue + * + * @param callable(TValue, TKey): (\Illuminate\Support\Collection|array) $callback + * @return static + */ + public function flatMap(callable $callback); + + /** + * Map the values into a new class. + * + * @template TMapIntoValue + * + * @param class-string $class + * @return static + */ + public function mapInto($class); + + /** + * Merge the collection with the given items. + * + * @template TMergeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function merge($items); + + /** + * Recursively merge the collection with the given items. + * + * @template TMergeRecursiveValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function mergeRecursive($items); + + /** + * Create a collection by using this collection for keys and another for its values. + * + * @template TCombineValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function combine($values); + + /** + * Union the collection with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function union($items); + + /** + * Get the min value of a given key. + * + * @template TMinResult = mixed + * + * @param (callable(TValue): TMinResult)|string|null $callback + * @return ($callback is callable ? ?TMinResult : ($callback is null ? ?TValue : mixed)) + */ + public function min($callback = null); + + /** + * Get the max value of a given key. + * + * @template TMaxResult = mixed + * + * @param (callable(TValue): TMaxResult)|string|null $callback + * @return ($callback is callable ? ?TMaxResult : ($callback is null ? ?TValue : mixed)) + */ + public function max($callback = null); + + /** + * Create a new collection consisting of every n-th element. + * + * @param int $step + * @param int $offset + * @return static + */ + public function nth($step, $offset = 0); + + /** + * Get the items with the specified keys. + * + * @param \Illuminate\Support\Enumerable|array|string $keys + * @return static + */ + public function only($keys); + + /** + * "Paginate" the collection by slicing it into a smaller collection. + * + * @param int $page + * @param int $perPage + * @return static + */ + public function forPage($page, $perPage); + + /** + * Partition the collection into two arrays using the given callback or key. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return static, static> + */ + public function partition($key, $operator = null, $value = null); + + /** + * Push all of the given items onto the collection. + * + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static + */ + public function concat($source); + + /** + * Get one or a specified number of items randomly from the collection. + * + * @param int|null $number + * @return ($number is null ? TValue : static) + * + * @throws \InvalidArgumentException + */ + public function random($number = null); + + /** + * Reduce the collection to a single value. + * + * @template TReduceInitial + * @template TReduceReturnType + * + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * @return TReduceInitial|TReduceReturnType + */ + public function reduce(callable $callback, $initial = null); + + /** + * Reduce the collection to a single value by mutating an initial value. + * + * @template TReduceIntoInitial + * + * @param TReduceIntoInitial $initial + * @param callable(TReduceIntoInitial, TValue, TKey): void $callback + * @return TReduceIntoInitial + */ + public function reduceInto($initial, callable $callback); + + /** + * Reduce the collection to multiple aggregate values. + * + * @param callable $callback + * @param mixed ...$initial + * @return array + * + * @throws \UnexpectedValueException + */ + public function reduceSpread(callable $callback, ...$initial); + + /** + * Replace the collection items with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function replace($items); + + /** + * Recursively replace the collection items with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function replaceRecursive($items); + + /** + * Reverse items order. + * + * @return static + */ + public function reverse(); + + /** + * Search the collection for a given value and return the corresponding key if successful. + * + * @param TValue|callable(TValue,TKey): bool $value + * @param bool $strict + * @return TKey|false + */ + public function search($value, $strict = false); + + /** + * Get the item before the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function before($value, $strict = false); + + /** + * Get the item after the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function after($value, $strict = false); + + /** + * Shuffle the items in the collection. + * + * @return static + */ + public function shuffle(); + + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param int $size + * @param int $step + * @return static + */ + public function sliding($size = 2, $step = 1); + + /** + * Skip the first {$count} items. + * + * @param int $count + * @return static + */ + public function skip($count); + + /** + * Skip items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipUntil($value); + + /** + * Skip items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipWhile($value); + + /** + * Get a slice of items from the enumerable. + * + * @param int $offset + * @param int|null $length + * @return static + */ + public function slice($offset, $length = null); + + /** + * Split a collection into a certain number of groups. + * + * @param int $numberOfGroups + * @return static + */ + public function split($numberOfGroups); + + /** + * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + * @throws \Illuminate\Support\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null); + + /** + * Get the first item in the collection but throw an exception if no matching items exist. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + */ + public function firstOrFail($key = null, $operator = null, $value = null); + + /** + * Chunk the collection into chunks of the given size. + * + * @param int $size + * @return static + */ + public function chunk($size); + + /** + * Chunk the collection into chunks with a callback. + * + * @param callable(TValue, TKey, static): bool $callback + * @return static> + */ + public function chunkWhile(callable $callback); + + /** + * Chunk the collection into chunks by comparing adjacent values using the given key or callback. + * + * @param (callable(TValue, TKey): mixed)|string $key + * @return static> + */ + public function chunkBy($key); + + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return static + */ + public function splitIn($numberOfGroups); + + /** + * Sort through each item with a callback. + * + * @param (callable(TValue, TValue): int)|null|int $callback + * @return static + */ + public function sort($callback = null); + + /** + * Sort items in descending order. + * + * @param int-mask-of $options + * @return static + */ + public function sortDesc($options = SORT_REGULAR); + + /** + * Sort the collection using the given callback. + * + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options + * @param bool $descending + * @return static + */ + public function sortBy($callback, $options = SORT_REGULAR, $descending = false); + + /** + * Sort the collection in descending order using the given callback. + * + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options + * @return static + */ + public function sortByDesc($callback, $options = SORT_REGULAR); + + /** + * Sort the collection keys. + * + * @param int-mask-of $options + * @param bool $descending + * @return static + */ + public function sortKeys($options = SORT_REGULAR, $descending = false); + + /** + * Sort the collection keys in descending order. + * + * @param int-mask-of $options + * @return static + */ + public function sortKeysDesc($options = SORT_REGULAR); + + /** + * Sort the collection keys using a callback. + * + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function sortKeysUsing(callable $callback); + + /** + * Get the sum of the given values. + * + * @param (callable(TValue, TKey): mixed)|string|null $callback + * @return mixed + */ + public function sum($callback = null); + + /** + * Take the first or last {$limit} items. + * + * @param int $limit + * @return static + */ + public function take($limit); + + /** + * Take items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeUntil($value); + + /** + * Take items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeWhile($value); + + /** + * Pass the collection to the given callback and then return it. + * + * @param callable(TValue): mixed $callback + * @return $this + */ + public function tap(callable $callback); + + /** + * Pass the enumerable to the given callback and return the result. + * + * @template TPipeReturnType + * + * @param callable($this): TPipeReturnType $callback + * @return TPipeReturnType + */ + public function pipe(callable $callback); + + /** + * Pass the collection into a new class. + * + * @template TPipeIntoValue + * + * @param class-string $class + * @return TPipeIntoValue + */ + public function pipeInto($class); + + /** + * Pass the collection through a series of callable pipes and return the result. + * + * @param array $pipes + * @return mixed + */ + public function pipeThrough($pipes); + + /** + * Get the values of a given key. + * + * @param string|array $value + * @param string|null $key + * @return static + */ + public function pluck($value, $key = null); + + /** + * Create a collection of all elements that do not pass a given truth test. + * + * @param (callable(TValue, TKey): bool)|bool|TValue $callback + * @return static + */ + public function reject($callback = true); + + /** + * Convert a flatten "dot" notation array into an expanded array. + * + * @return static + */ + public function undot(); + + /** + * Return only unique items from the collection array. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @param bool $strict + * @return static + */ + public function unique($key = null, $strict = false); + + /** + * Return only unique items from the collection array using strict comparison. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @return static + */ + public function uniqueStrict($key = null); + + /** + * Reset the keys on the underlying array. + * + * @return static + */ + public function values(); + + /** + * Pad collection to the specified length with a value. + * + * @template TPadValue + * + * @param int $size + * @param TPadValue $value + * @return static + */ + public function pad($size, $value); + + /** + * Get the values iterator. + * + * @return \Traversable + */ + public function getIterator(): Traversable; + + /** + * Count the number of items in the collection. + * + * @return int + */ + public function count(): int; + + /** + * Count the number of items in the collection by a field or using a callback. + * + * @param (callable(TValue, TKey): (array-key|\UnitEnum))|string|null $countBy + * @return static + */ + public function countBy($countBy = null); + + /** + * Zip the collection together with one or more arrays. + * + * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]); + * => [[1, 4], [2, 5], [3, 6]] + * + * @template TZipValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$items + * @return static> + */ + public function zip($items); + + /** + * Collect the values into a collection. + * + * @return \Illuminate\Support\Collection + */ + public function collect(); + + /** + * Get the collection of items as a plain array. + * + * @return array + */ + public function toArray(); + + /** + * Convert the object into something JSON serializable. + * + * @return mixed + */ + public function jsonSerialize(): mixed; + + /** + * Get the collection of items as JSON. + * + * @param int $options + * @return string + */ + public function toJson($options = 0); + + /** + * Get the collection of items as pretty print formatted JSON. + * + * @param int $options + * @return string + */ + public function toPrettyJson(int $options = 0); + + /** + * Get a CachingIterator instance. + * + * @param int $flags + * @return \CachingIterator + */ + public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING); + + /** + * Convert the collection to its string representation. + * + * @return string + */ + public function __toString(); + + /** + * Indicate that the model's string representation should be escaped when __toString is invoked. + * + * @param bool $escape + * @return $this + */ + public function escapeWhenCastingToString($escape = true); + + /** + * Add a method to the list of proxied methods. + * + * @param string $method + * @return void + */ + public static function proxy($method); + + /** + * Dynamically access collection proxies. + * + * @param string $key + * @return mixed + * + * @throws \Exception + */ + public function __get($key); +} diff --git a/src/Illuminate/Support/HigherOrderCollectionProxy.php b/src/Illuminate/Support/HigherOrderCollectionProxy.php new file mode 100644 index 00000000..035d0fda --- /dev/null +++ b/src/Illuminate/Support/HigherOrderCollectionProxy.php @@ -0,0 +1,69 @@ + + * @mixin TValue + */ +class HigherOrderCollectionProxy +{ + /** + * The collection being operated on. + * + * @var \Illuminate\Support\Enumerable + */ + protected $collection; + + /** + * The method being proxied. + * + * @var string + */ + protected $method; + + /** + * Create a new proxy instance. + * + * @param \Illuminate\Support\Enumerable $collection + * @param string $method + */ + public function __construct(Enumerable $collection, $method) + { + $this->method = $method; + $this->collection = $collection; + } + + /** + * Proxy accessing an attribute onto the collection items. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->collection->{$this->method}(function ($value) use ($key) { + return is_array($value) ? $value[$key] : $value->{$key}; + }); + } + + /** + * Proxy a method call onto the collection items. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { + return is_string($value) + ? $value::{$method}(...$parameters) + : $value->{$method}(...$parameters); + }); + } +} diff --git a/src/Illuminate/Support/ItemNotFoundException.php b/src/Illuminate/Support/ItemNotFoundException.php new file mode 100644 index 00000000..05a51d95 --- /dev/null +++ b/src/Illuminate/Support/ItemNotFoundException.php @@ -0,0 +1,9 @@ + + */ +class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable +{ + /** + * @use \Illuminate\Support\Traits\EnumeratesValues + */ + use EnumeratesValues, Macroable; + + /** + * The source from which to generate items. + * + * @var (Closure(): \Generator)|static|array + */ + public $source; + + /** + * Create a new lazy collection instance. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|(Closure(): \Generator)|self|array|null $source + * + * @throws \InvalidArgumentException + */ + public function __construct($source = null) + { + if ($source instanceof Closure || $source instanceof self) { + $this->source = $source; + } elseif (is_null($source)) { + $this->source = static::empty(); + } elseif ($source instanceof Generator) { + throw new InvalidArgumentException( + 'Generators should not be passed directly to LazyCollection. Instead, pass a generator function.' + ); + } else { + $this->source = $this->getArrayableItems($source); + } + } + + /** + * Create a new instance of the collection. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|(Closure(): \Generator)|self|array|null $items + * @return static + */ + protected function newInstance($items = []) + { + return new static($items); + } + + /** + * Create a new collection instance if the value isn't one already. + * + * @template TMakeKey of array-key + * @template TMakeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|(Closure(): \Generator)|self|array|null $items + * @return static + */ + public static function make($items = [], ...$args) + { + return new static($items, ...$args); + } + + /** + * Create a collection with the given range. + * + * @param int $from + * @param int $to + * @param int $step + * @return ($step is 0 ? never : static) + * + * @throws \InvalidArgumentException + */ + public static function range($from, $to, $step = 1, ...$args) + { + if ($step == 0) { + throw new InvalidArgumentException('Step value cannot be zero.'); + } + + return new static(function () use ($from, $to, $step) { + if ($from <= $to) { + for (; $from <= $to; $from += abs($step)) { + yield $from; + } + } else { + for (; $from >= $to; $from -= abs($step)) { + yield $from; + } + } + }); + } + + /** + * Get all items in the enumerable. + * + * @return array + */ + public function all() + { + if (is_array($this->source)) { + return $this->source; + } + + return iterator_to_array($this->getIterator()); + } + + /** + * Eager load all items into a new lazy collection backed by an array. + * + * @return static + */ + public function eager() + { + return new static($this->all()); + } + + /** + * Cache values as they're enumerated. + * + * @return static + */ + public function remember() + { + $iterator = $this->getIterator(); + + $iteratorIndex = 0; + + $cache = []; + + return new static(function () use ($iterator, &$iteratorIndex, &$cache) { + for ($index = 0; true; $index++) { + if (array_key_exists($index, $cache)) { + yield $cache[$index][0] => $cache[$index][1]; + + continue; + } + + if ($iteratorIndex < $index) { + $iterator->next(); + + $iteratorIndex++; + } + + if (! $iterator->valid()) { + break; + } + + $cache[$index] = [$iterator->key(), $iterator->current()]; + + yield $cache[$index][0] => $cache[$index][1]; + } + }); + } + + /** + * Get the median of a given key. + * + * @param string|array|null $key + * @return float|int|null + */ + public function median($key = null) + { + return $this->collect()->median($key); + } + + /** + * Get the mode of a given key. + * + * @param string|array|null $key + * @return array|null + */ + public function mode($key = null) + { + return $this->collect()->mode($key); + } + + /** + * Collapse the collection of items into a single array. + * + * @return static + */ + public function collapse() + { + return new static(function () { + foreach ($this as $values) { + if (is_array($values) || $values instanceof Enumerable) { + foreach ($values as $value) { + yield $value; + } + } + } + }); + } + + /** + * Collapse the collection of items into a single array while preserving its keys. + * + * @return static + */ + public function collapseWithKeys() + { + return new static(function () { + foreach ($this as $values) { + if (is_array($values) || $values instanceof Enumerable) { + foreach ($values as $key => $value) { + yield $key => $value; + } + } + } + }); + } + + /** + * Determine if an item exists in the enumerable. + * + * @param (callable(TValue, TKey): bool)|TValue|string $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)) { + $placeholder = new stdClass; + + /** @var callable $key */ + return $this->first($key, $placeholder) !== $placeholder; + } + + if (func_num_args() === 1) { + $needle = $key; + + foreach ($this as $value) { + if ($value == $needle) { + return true; + } + } + + return false; + } + + return $this->contains($this->operatorForWhere(...func_get_args())); + } + + /** + * Determine if an item exists, using strict comparison. + * + * @param (callable(TValue): bool)|TValue|array-key $key + * @param TValue|null $value + * @return bool + */ + public function containsStrict($key, $value = null) + { + if (func_num_args() === 2) { + return $this->contains(fn ($item) => data_get($item, $key) === $value); + } + + if ($this->useAsCallable($key)) { + return ! is_null($this->first($key)); + } + + foreach ($this as $item) { + if ($item === $key) { + return true; + } + } + + return false; + } + + /** + * Determine if an item is not contained in the enumerable. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null) + { + return ! $this->contains(...func_get_args()); + } + + /** + * Determine if an item is not contained in the enumerable, using strict comparison. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContainStrict($key, $operator = null, $value = null) + { + return ! $this->containsStrict(...func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function crossJoin(...$arrays) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function countBy($countBy = null) + { + $countBy = is_null($countBy) + ? $this->identity() + : $this->valueRetriever($countBy); + + return new static(function () use ($countBy) { + $counts = []; + + foreach ($this as $key => $value) { + $group = enum_value($countBy($value, $key)); + + if (empty($counts[$group])) { + $counts[$group] = 0; + } + + $counts[$group]++; + } + + yield from $counts; + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diff($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diffUsing($items, callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diffAssoc($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diffAssocUsing($items, callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diffKeys($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function diffKeysUsing($items, callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function duplicates($callback = null, $strict = false) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function duplicatesStrict($callback = null) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function except($keys) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Run a filter over each of the items. + * + * @param (callable(TValue, TKey): bool)|null $callback + * @return static + */ + public function filter(?callable $callback = null) + { + if (is_null($callback)) { + $callback = fn ($value) => (bool) $value; + } + + return new static(function () use ($callback) { + foreach ($this as $key => $value) { + if ($callback($value, $key)) { + yield $key => $value; + } + } + }); + } + + /** + * Get the first item from the enumerable passing the given truth test. + * + * @template TFirstDefault + * + * @param (callable(TValue): bool)|null $callback + * @param TFirstDefault|(\Closure(): TFirstDefault) $default + * @return TValue|TFirstDefault + */ + public function first(?callable $callback = null, $default = null) + { + $iterator = $this->getIterator(); + + if (is_null($callback)) { + if (! $iterator->valid()) { + return value($default); + } + + return $iterator->current(); + } + + foreach ($iterator as $key => $value) { + if ($callback($value, $key)) { + return $value; + } + } + + return value($default); + } + + /** + * Get a flattened list of the items in the collection. + * + * @param int $depth + * @return static + */ + public function flatten($depth = INF) + { + $instance = new static(function () use ($depth) { + foreach ($this as $item) { + if (! is_array($item) && ! $item instanceof Enumerable) { + yield $item; + } elseif ($depth === 1) { + yield from $item; + } else { + yield from (new static($item))->flatten($depth - 1); + } + } + }); + + return $instance->values(); + } + + /** + * Flip the items in the collection. + * + * @return static + */ + public function flip() + { + return new static(function () { + foreach ($this as $key => $value) { + if (is_string($value) || is_int($value)) { + yield $value => $key; + } + } + }); + } + + /** + * Get an item by key. + * + * @template TGetDefault + * + * @param TKey|null $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TValue|TGetDefault + */ + public function get($key, $default = null) + { + if (is_null($key)) { + return; + } + + foreach ($this as $outerKey => $outerValue) { + if ($outerKey == $key) { + return $outerValue; + } + } + + return value($default); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function groupBy($groupBy, $preserveKeys = false) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function keyBy($keyBy) + { + return new static(function () use ($keyBy) { + $keyBy = $this->valueRetriever($keyBy); + + foreach ($this as $key => $item) { + $resolvedKey = $keyBy($item, $key); + + if ($resolvedKey instanceof \UnitEnum) { + $resolvedKey = enum_value($resolvedKey); + } + + if (is_object($resolvedKey) || is_null($resolvedKey)) { + $resolvedKey = (string) $resolvedKey; + } + + yield $resolvedKey => $item; + } + }); + } + + /** + * Determine if an item exists in the collection by key. + * + * @param mixed $key + * @return bool + */ + public function has($key) + { + $keys = array_flip(is_array($key) ? $key : func_get_args()); + + foreach ($this as $key => $value) { + unset($keys[$key]); + + if (empty($keys)) { + return true; + } + } + + return false; + } + + /** + * Determine if any of the keys exist in the collection. + * + * @param mixed $key + * @return bool + */ + public function hasAny($key) + { + $keys = array_flip(is_array($key) ? $key : func_get_args()); + + foreach ($this as $key => $value) { + if (array_key_exists($key, $keys)) { + return true; + } + } + + return false; + } + + /** + * Concatenate values of a given key as a string. + * + * @param (callable(TValue, TKey): mixed)|string $value + * @param string|null $glue + * @return string + */ + public function implode($value, $glue = null) + { + return $this->collect()->implode(...func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function intersect($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function intersectUsing($items, callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function intersectAssoc($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function intersectAssocUsing($items, callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function intersectByKeys($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Determine if the items are empty or not. + * + * @return bool + */ + public function isEmpty() + { + return ! $this->getIterator()->valid(); + } + + /** + * Determine if the collection contains a single item. + * + * @param (callable(TValue, TKey): bool)|null $callback + * @return bool + * + * @deprecated 12.49.0 Use the `hasSole()` method instead. + */ + public function containsOneItem(?callable $callback = null): bool + { + return $this->hasSole($callback); + } + + /** + * Determine if the collection contains multiple items. + * + * @return bool + * + * @deprecated 12.50.0 Use the `hasMany()` method instead. + */ + public function containsManyItems(): bool + { + return $this->hasMany(); + } + + /** + * Join all items from the collection using a string. The final items can use a separate glue string. + * + * @param string $glue + * @param string $finalGlue + * @return string + */ + public function join($glue, $finalGlue = '') + { + return $this->collect()->join(...func_get_args()); + } + + /** + * Get the keys of the collection items. + * + * @return static + */ + public function keys() + { + return new static(function () { + foreach ($this as $key => $value) { + yield $key; + } + }); + } + + /** + * Get the last item from the collection. + * + * @template TLastDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TLastDefault|(\Closure(): TLastDefault) $default + * @return TValue|TLastDefault + */ + public function last(?callable $callback = null, $default = null) + { + $needle = $placeholder = new stdClass; + + foreach ($this as $key => $value) { + if (is_null($callback) || $callback($value, $key)) { + $needle = $value; + } + } + + return $needle === $placeholder ? value($default) : $needle; + } + + /** + * Get the values of a given key. + * + * @param string|array $value + * @param string|null $key + * @return static + */ + public function pluck($value, $key = null) + { + return new static(function () use ($value, $key) { + [$value, $key] = $this->explodePluckParameters($value, $key); + + foreach ($this as $item) { + $itemValue = $value instanceof Closure + ? $value($item) + : data_get($item, $value); + + if (is_null($key)) { + yield $itemValue; + } else { + $itemKey = $key instanceof Closure + ? $key($item) + : data_get($item, $key); + + if (is_object($itemKey) && method_exists($itemKey, '__toString')) { + $itemKey = (string) $itemKey; + } + + yield $itemKey => $itemValue; + } + } + }); + } + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return static + */ + public function map(callable $callback) + { + return new static(function () use ($callback) { + foreach ($this as $key => $value) { + yield $key => $callback($value, $key); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function mapToDictionary(callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * 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(TValue, TKey): array $callback + * @return static + */ + public function mapWithKeys(callable $callback) + { + return new static(function () use ($callback) { + foreach ($this as $key => $value) { + yield from $callback($value, $key); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function merge($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function mergeRecursive($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Multiply the items in the collection by the multiplier. + * + * @param int $multiplier + * @return static + */ + public function multiply(int $multiplier) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Create a collection by using this collection for keys and another for its values. + * + * @template TCombineValue + * + * @param \IteratorAggregate|array|(callable(): \Generator) $values + * @return static + */ + public function combine($values) + { + return new static(function () use ($values) { + $values = $this->makeIterator($values); + + $errorMessage = 'Both parameters should have an equal number of elements'; + + foreach ($this as $key) { + if (! $values->valid()) { + trigger_error($errorMessage, E_USER_WARNING); + + break; + } + + yield $key => $values->current(); + + $values->next(); + } + + if ($values->valid()) { + trigger_error($errorMessage, E_USER_WARNING); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function union($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Create a new collection consisting of every n-th element. + * + * @param int $step + * @param int $offset + * @return ($step is positive-int ? static : never) + * + * @throws \InvalidArgumentException + */ + public function nth($step, $offset = 0) + { + if ($step < 1) { + throw new InvalidArgumentException('Step value must be at least 1.'); + } + + return new static(function () use ($step, $offset) { + $position = 0; + + foreach ($this->slice($offset) as $item) { + if ($position % $step === 0) { + yield $item; + } + + $position++; + } + }); + } + + /** + * Get the items with the specified keys. + * + * @param \Illuminate\Support\Enumerable|array|string $keys + * @return static + */ + public function only($keys) + { + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } elseif (! is_null($keys)) { + $keys = is_array($keys) ? $keys : func_get_args(); + } + + return new static(function () use ($keys) { + if (is_null($keys)) { + yield from $this; + } else { + $keys = array_flip($keys); + + foreach ($this as $key => $value) { + if (array_key_exists($key, $keys)) { + yield $key => $value; + + unset($keys[$key]); + + if (empty($keys)) { + break; + } + } + } + } + }); + } + + /** + * Select specific values from the items within the collection. + * + * @param \Illuminate\Support\Enumerable|array|string $keys + * @return static + */ + public function select($keys) + { + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } elseif (! is_null($keys)) { + $keys = is_array($keys) ? $keys : func_get_args(); + } + + return new static(function () use ($keys) { + if (is_null($keys)) { + yield from $this; + } else { + foreach ($this as $item) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + yield $result; + } + } + }); + } + + /** + * Push all of the given items onto the collection. + * + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static + */ + public function concat($source) + { + return (new static(function () use ($source) { + yield from $this; + yield from $source; + }))->values(); + } + + /** + * Get one or a specified number of items randomly from the collection. + * + * @param int|null $number + * @param bool $preserveKeys + * @return ($number is null ? TValue : static) + * + * @throws \InvalidArgumentException + */ + public function random($number = null, $preserveKeys = false) + { + $result = $this->collect()->random(...func_get_args()); + + return is_null($number) ? $result : new static($result); + } + + /** + * Replace the collection items with the given items. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static + */ + public function replace($items) + { + return new static(function () use ($items) { + $items = $this->getArrayableItems($items); + + foreach ($this as $key => $value) { + if (array_key_exists($key, $items)) { + yield $key => $items[$key]; + + unset($items[$key]); + } else { + yield $key => $value; + } + } + + foreach ($items as $key => $value) { + yield $key => $value; + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function replaceRecursive($items) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function reverse() + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Search the collection for a given value and return the corresponding key if successful. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TKey|false + */ + public function search($value, $strict = false) + { + /** @var (callable(TValue,TKey): bool) $predicate */ + $predicate = $this->useAsCallable($value) + ? $value + : function ($item) use ($value, $strict) { + return $strict ? $item === $value : $item == $value; + }; + + foreach ($this as $key => $item) { + if ($predicate($item, $key)) { + return $key; + } + } + + return false; + } + + /** + * Get the item before the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function before($value, $strict = false) + { + $previous = null; + + /** @var (callable(TValue,TKey): bool) $predicate */ + $predicate = $this->useAsCallable($value) + ? $value + : function ($item) use ($value, $strict) { + return $strict ? $item === $value : $item == $value; + }; + + foreach ($this as $key => $item) { + if ($predicate($item, $key)) { + return $previous; + } + + $previous = $item; + } + + return null; + } + + /** + * Get the item after the given item. + * + * @param TValue|(callable(TValue,TKey): bool) $value + * @param bool $strict + * @return TValue|null + */ + public function after($value, $strict = false) + { + $found = false; + + /** @var (callable(TValue,TKey): bool) $predicate */ + $predicate = $this->useAsCallable($value) + ? $value + : function ($item) use ($value, $strict) { + return $strict ? $item === $value : $item == $value; + }; + + foreach ($this as $key => $item) { + if ($found) { + return $item; + } + + if ($predicate($item, $key)) { + $found = true; + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function shuffle() + { + return $this->passthru(__FUNCTION__, []); + } + + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param positive-int $size + * @param positive-int $step + * @return static + * + * @throws \InvalidArgumentException + */ + public function sliding($size = 2, $step = 1) + { + if ($size < 1) { + throw new InvalidArgumentException('Size value must be at least 1.'); + } elseif ($step < 1) { + throw new InvalidArgumentException('Step value must be at least 1.'); + } + + return new static(function () use ($size, $step) { + $iterator = $this->getIterator(); + + $chunk = []; + + while ($iterator->valid()) { + $chunk[$iterator->key()] = $iterator->current(); + + if (count($chunk) == $size) { + yield (new static($chunk))->tap(function () use (&$chunk, $step) { + $chunk = array_slice($chunk, $step, null, true); + }); + + // If the $step between chunks is bigger than each chunk's $size + // we will skip the extra items (which should never be in any + // chunk) before we continue to the next chunk in the loop. + if ($step > $size) { + $skip = $step - $size; + + for ($i = 0; $i < $skip && $iterator->valid(); $i++) { + $iterator->next(); + } + } + } + + $iterator->next(); + } + }); + } + + /** + * Skip the first {$count} items. + * + * @param int $count + * @return static + */ + public function skip($count) + { + return new static(function () use ($count) { + $iterator = $this->getIterator(); + + while ($iterator->valid() && $count--) { + $iterator->next(); + } + + while ($iterator->valid()) { + yield $iterator->key() => $iterator->current(); + + $iterator->next(); + } + }); + } + + /** + * Skip items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipUntil($value) + { + $callback = $this->useAsCallable($value) ? $value : $this->equality($value); + + return $this->skipWhile($this->negate($callback)); + } + + /** + * Skip items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipWhile($value) + { + $callback = $this->useAsCallable($value) ? $value : $this->equality($value); + + return new static(function () use ($callback) { + $iterator = $this->getIterator(); + + while ($iterator->valid() && $callback($iterator->current(), $iterator->key())) { + $iterator->next(); + } + + while ($iterator->valid()) { + yield $iterator->key() => $iterator->current(); + + $iterator->next(); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function slice($offset, $length = null) + { + if ($offset < 0 || $length < 0) { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + $instance = $this->skip($offset); + + return is_null($length) ? $instance : $instance->take($length); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + #[\Override] + public function split($numberOfGroups) + { + if ($numberOfGroups < 1) { + throw new InvalidArgumentException('Number of groups must be at least 1.'); + } + + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + * @throws \Illuminate\Support\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->unless($filter == null) + ->filter($filter) + ->take(2) + ->collect() + ->sole(); + } + + /** + * Determine if the collection contains a single item or a single item matching the given criteria. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function hasSole($key = null, $operator = null, $value = null): bool + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->unless($filter == null) + ->filter($filter) + ->take(2) + ->count() === 1; + } + + /** + * Get the first item in the collection but throw an exception if no matching items exist. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + */ + public function firstOrFail($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->unless($filter == null) + ->filter($filter) + ->take(1) + ->collect() + ->firstOrFail(); + } + + /** + * Chunk the collection into chunks of the given size. + * + * @param int $size + * @param bool $preserveKeys + * @return ($preserveKeys is true ? static : static>) + */ + public function chunk($size, $preserveKeys = true) + { + if ($size <= 0) { + return static::empty(); + } + + $add = match ($preserveKeys) { + true => fn (array &$chunk, Traversable $iterator) => $chunk[$iterator->key()] = $iterator->current(), + false => fn (array &$chunk, Traversable $iterator) => $chunk[] = $iterator->current(), + }; + + return new static(function () use ($size, $add) { + $iterator = $this->getIterator(); + + while ($iterator->valid()) { + $chunk = []; + + while (true) { + $add($chunk, $iterator); + + if (count($chunk) < $size) { + $iterator->next(); + + if (! $iterator->valid()) { + break; + } + } else { + break; + } + } + + yield new static($chunk); + + $iterator->next(); + } + }); + } + + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return ($numberOfGroups is positive-int ? static : never) + * + * @throws \InvalidArgumentException + */ + public function splitIn($numberOfGroups) + { + if ($numberOfGroups < 1) { + throw new InvalidArgumentException('Number of groups must be at least 1.'); + } + + return $this->chunk((int) ceil($this->count() / $numberOfGroups)); + } + + /** + * Chunk the collection into chunks with a callback. + * + * @param callable(TValue, TKey, Collection): bool $callback + * @return static> + */ + public function chunkWhile(callable $callback) + { + return new static(function () use ($callback) { + $iterator = $this->getIterator(); + + $chunk = new Collection; + + if ($iterator->valid()) { + $chunk[$iterator->key()] = $iterator->current(); + + $iterator->next(); + } + + while ($iterator->valid()) { + if (! $callback($iterator->current(), $iterator->key(), $chunk)) { + yield new static($chunk); + + $chunk = new Collection; + } + + $chunk[$iterator->key()] = $iterator->current(); + + $iterator->next(); + } + + if ($chunk->isNotEmpty()) { + yield new static($chunk); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sort($callback = null) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortDesc($options = SORT_REGULAR) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortBy($callback, $options = SORT_REGULAR, $descending = false) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortByDesc($callback, $options = SORT_REGULAR) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortKeys($options = SORT_REGULAR, $descending = false) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortKeysDesc($options = SORT_REGULAR) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function sortKeysUsing(callable $callback) + { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + /** + * Take the first or last {$limit} items. + * + * @param int $limit + * @return static + */ + public function take($limit) + { + if ($limit < 0) { + return new static(function () use ($limit) { + $limit = abs($limit); + $ringBuffer = []; + $position = 0; + + foreach ($this as $key => $value) { + $ringBuffer[$position] = [$key, $value]; + $position = ($position + 1) % $limit; + } + + for ($i = 0, $end = min($limit, count($ringBuffer)); $i < $end; $i++) { + $pointer = ($position + $i) % $limit; + yield $ringBuffer[$pointer][0] => $ringBuffer[$pointer][1]; + } + }); + } + + return new static(function () use ($limit) { + $iterator = $this->getIterator(); + + while ($limit--) { + if (! $iterator->valid()) { + break; + } + + yield $iterator->key() => $iterator->current(); + + if ($limit) { + $iterator->next(); + } + } + }); + } + + /** + * Take items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeUntil($value) + { + /** @var callable(TValue, TKey): bool $callback */ + $callback = $this->useAsCallable($value) ? $value : $this->equality($value); + + return new static(function () use ($callback) { + foreach ($this as $key => $item) { + if ($callback($item, $key)) { + break; + } + + yield $key => $item; + } + }); + } + + /** + * Take items in the collection until a given point in time, with an optional callback on timeout. + * + * @param \DateTimeInterface $timeout + * @param callable(TValue|null, TKey|null): mixed|null $callback + * @return static + */ + public function takeUntilTimeout(DateTimeInterface $timeout, ?callable $callback = null) + { + $timeout = $timeout->getTimestamp(); + + return new static(function () use ($timeout, $callback) { + if ($this->now() >= $timeout) { + if ($callback) { + $callback(null, null); + } + + return; + } + + foreach ($this as $key => $value) { + yield $key => $value; + + if ($this->now() >= $timeout) { + if ($callback) { + $callback($value, $key); + } + + break; + } + } + }); + } + + /** + * Take items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeWhile($value) + { + /** @var callable(TValue, TKey): bool $callback */ + $callback = $this->useAsCallable($value) ? $value : $this->equality($value); + + return $this->takeUntil(fn ($item, $key) => ! $callback($item, $key)); + } + + /** + * Pass each item in the collection to the given callback, lazily. + * + * @param callable(TValue, TKey): mixed $callback + * @return static + */ + public function tapEach(callable $callback) + { + return new static(function () use ($callback) { + foreach ($this as $key => $value) { + $callback($value, $key); + + yield $key => $value; + } + }); + } + + /** + * Throttle the values, releasing them at most once per the given seconds. + * + * @return static + */ + public function throttle(float $seconds) + { + return new static(function () use ($seconds) { + $microseconds = $seconds * 1_000_000; + + foreach ($this as $key => $value) { + $fetchedAt = $this->preciseNow(); + + yield $key => $value; + + $sleep = $microseconds - ($this->preciseNow() - $fetchedAt); + + $this->usleep((int) $sleep); + } + }); + } + + /** + * Flatten a multi-dimensional associative array with dots. + * + * @param int $depth + * @return static + */ + public function dot($depth = INF) + { + return $this->passthru(__FUNCTION__, [$depth]); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function undot() + { + return $this->passthru(__FUNCTION__, []); + } + + /** + * Return only unique items from the collection array. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @param bool $strict + * @return static + */ + public function unique($key = null, $strict = false) + { + $callback = $this->valueRetriever($key); + + return new static(function () use ($callback, $strict) { + $exists = []; + + foreach ($this as $key => $item) { + if (! in_array($id = $callback($item, $key), $exists, $strict)) { + yield $key => $item; + + $exists[] = $id; + } + } + }); + } + + /** + * Reset the keys on the underlying array. + * + * @return static + */ + public function values() + { + return new static(function () { + foreach ($this as $item) { + yield $item; + } + }); + } + + /** + * Run the given callback every time the interval has passed. + * + * @return static + */ + public function withHeartbeat(DateInterval|int $interval, callable $callback) + { + $seconds = is_int($interval) ? $interval : $this->intervalSeconds($interval); + + return new static(function () use ($seconds, $callback) { + $start = $this->now(); + + foreach ($this as $key => $value) { + $now = $this->now(); + + if (($now - $start) >= $seconds) { + $callback(); + + $start = $now; + } + + yield $key => $value; + } + }); + } + + /** + * Get the total seconds from the given interval. + */ + protected function intervalSeconds(DateInterval $interval): int + { + $start = new DateTimeImmutable(); + + return $start->add($interval)->getTimestamp() - $start->getTimestamp(); + } + + /** + * Zip the collection together with one or more arrays. + * + * e.g. new LazyCollection([1, 2, 3])->zip([4, 5, 6]); + * => [[1, 4], [2, 5], [3, 6]] + * + * @template TZipValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$items + * @return static> + */ + public function zip($items) + { + $iterables = func_get_args(); + + return new static(function () use ($iterables) { + $iterators = (new Collection($iterables)) + ->map(fn ($iterable) => $this->makeIterator($iterable)) + ->prepend($this->getIterator()); + + while ($iterators->contains->valid()) { + yield new static($iterators->map->current()); + + $iterators->each->next(); + } + }); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function pad($size, $value) + { + if ($size < 0) { + return $this->passthru(__FUNCTION__, func_get_args()); + } + + return new static(function () use ($size, $value) { + $yielded = 0; + + foreach ($this as $index => $item) { + yield $index => $item; + + $yielded++; + } + + while ($yielded++ < $size) { + yield $value; + } + }); + } + + /** + * Get the values iterator. + * + * @return \Traversable + */ + public function getIterator(): Traversable + { + return $this->makeIterator($this->source); + } + + /** + * Count the number of items in the collection. + * + * @return int + */ + public function count(): int + { + if (is_array($this->source)) { + return count($this->source); + } + + return iterator_count($this->getIterator()); + } + + /** + * Make an iterator from the given source. + * + * @template TIteratorKey of array-key + * @template TIteratorValue + * + * @param \IteratorAggregate|array|(callable(): \Generator) $source + * @return \Traversable + */ + protected function makeIterator($source) + { + if ($source instanceof IteratorAggregate) { + return $source->getIterator(); + } + + if (is_array($source)) { + return new ArrayIterator($source); + } + + if (is_callable($source)) { + $maybeTraversable = $source(); + + return $maybeTraversable instanceof Traversable + ? $maybeTraversable + : new ArrayIterator(Arr::wrap($maybeTraversable)); + } + + return new ArrayIterator((array) $source); + } + + /** + * Explode the "value" and "key" arguments passed to "pluck". + * + * @param string|string[] $value + * @param string|string[]|null $key + * @return array{string[],string[]|null} + */ + protected function explodePluckParameters($value, $key) + { + $value = is_string($value) ? explode('.', $value) : $value; + + $key = is_null($key) || is_array($key) || $key instanceof Closure ? $key : explode('.', $key); + + return [$value, $key]; + } + + /** + * Pass this lazy collection through a method on the collection class. + * + * @param string $method + * @param array $params + * @return static + */ + protected function passthru($method, array $params) + { + return new static(function () use ($method, $params) { + yield from $this->collect()->$method(...$params); + }); + } + + /** + * Get the current time. + * + * @return int + */ + protected function now() + { + return class_exists(Carbon::class) + ? Carbon::now()->getTimestamp() + : time(); + } + + /** + * Get the precise current time. + * + * @return float + */ + protected function preciseNow() + { + return class_exists(Carbon::class) + ? Carbon::now()->getPreciseTimestamp() + : microtime(true) * 1_000_000; + } + + /** + * Sleep for the given amount of microseconds. + * + * @return void + */ + protected function usleep(int $microseconds) + { + if ($microseconds <= 0) { + return; + } + + class_exists(Sleep::class) + ? Sleep::usleep($microseconds) + : usleep($microseconds); + } +} diff --git a/src/Illuminate/Support/MultipleItemsFoundException.php b/src/Illuminate/Support/MultipleItemsFoundException.php new file mode 100644 index 00000000..9c5c7c56 --- /dev/null +++ b/src/Illuminate/Support/MultipleItemsFoundException.php @@ -0,0 +1,39 @@ +count = $count; + + parent::__construct("$count items were found.", $code, $previous); + } + + /** + * Get the number of items found. + * + * @return int + */ + public function getCount() + { + return $this->count; + } +} diff --git a/src/Illuminate/Support/SortDirection.php b/src/Illuminate/Support/SortDirection.php new file mode 100644 index 00000000..5bc65053 --- /dev/null +++ b/src/Illuminate/Support/SortDirection.php @@ -0,0 +1,12 @@ +value/::from), so a pure enum is the whole contract. +enum SortDirection +{ + case Ascending; + case Descending; +} diff --git a/src/Illuminate/Support/Traits/EnumeratesValues.php b/src/Illuminate/Support/Traits/EnumeratesValues.php new file mode 100644 index 00000000..fbd7c90d --- /dev/null +++ b/src/Illuminate/Support/Traits/EnumeratesValues.php @@ -0,0 +1,1244 @@ + $average + * @property-read HigherOrderCollectionProxy $avg + * @property-read HigherOrderCollectionProxy $contains + * @property-read HigherOrderCollectionProxy $doesntContain + * @property-read HigherOrderCollectionProxy $each + * @property-read HigherOrderCollectionProxy $every + * @property-read HigherOrderCollectionProxy $filter + * @property-read HigherOrderCollectionProxy $first + * @property-read HigherOrderCollectionProxy $flatMap + * @property-read HigherOrderCollectionProxy $groupBy + * @property-read HigherOrderCollectionProxy $hasMany + * @property-read HigherOrderCollectionProxy $hasSole + * @property-read HigherOrderCollectionProxy $keyBy + * @property-read HigherOrderCollectionProxy $last + * @property-read HigherOrderCollectionProxy $map + * @property-read HigherOrderCollectionProxy $max + * @property-read HigherOrderCollectionProxy $min + * @property-read HigherOrderCollectionProxy $partition + * @property-read HigherOrderCollectionProxy $percentage + * @property-read HigherOrderCollectionProxy $reject + * @property-read HigherOrderCollectionProxy $skipUntil + * @property-read HigherOrderCollectionProxy $skipWhile + * @property-read HigherOrderCollectionProxy $some + * @property-read HigherOrderCollectionProxy $sortBy + * @property-read HigherOrderCollectionProxy $sortByDesc + * @property-read HigherOrderCollectionProxy $sum + * @property-read HigherOrderCollectionProxy $takeUntil + * @property-read HigherOrderCollectionProxy $takeWhile + * @property-read HigherOrderCollectionProxy $unique + * @property-read HigherOrderCollectionProxy $unless + * @property-read HigherOrderCollectionProxy $until + * @property-read HigherOrderCollectionProxy $when + */ +trait EnumeratesValues +{ + use Conditionable; + + /** + * Indicates that the object's string representation should be escaped when __toString is invoked. + * + * @var bool + */ + protected $escapeWhenCastingToString = false; + + /** + * The methods that can be proxied. + * + * @var array + */ + protected static $proxies = [ + 'average', + 'avg', + 'contains', + 'doesntContain', + 'each', + 'every', + 'filter', + 'first', + 'flatMap', + 'groupBy', + 'hasMany', + 'hasSole', + 'keyBy', + 'last', + 'map', + 'max', + 'min', + 'partition', + 'percentage', + 'reject', + 'skipUntil', + 'skipWhile', + 'some', + 'sortBy', + 'sortByDesc', + 'sum', + 'takeUntil', + 'takeWhile', + 'unique', + 'unless', + 'until', + 'when', + ]; + + /** + * Create a new collection instance if the value isn't one already. + * + * @template TMakeKey of array-key + * @template TMakeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static + */ + public static function make($items = [], ...$args) + { + return new static($items, ...$args); + } + + /** + * Wrap the given value in a collection if applicable. + * + * @template TWrapValue + * + * @param iterable|TWrapValue $value + * @return static + */ + public static function wrap($value, ...$args) + { + return $value instanceof Enumerable + ? new static($value, ...$args) + : new static(Arr::wrap($value), ...$args); + } + + /** + * Get the underlying items from the given collection if applicable. + * + * @template TUnwrapKey of array-key + * @template TUnwrapValue + * + * @param array|static $value + * @return array + */ + public static function unwrap($value) + { + return $value instanceof Enumerable ? $value->all() : $value; + } + + /** + * Create a new instance with no items. + * + * @return static + */ + public static function empty(...$args) + { + return new static([], ...$args); + } + + /** + * Create a new collection by invoking the callback a given amount of times. + * + * @template TTimesValue + * + * @param int $number + * @param (callable(int): TTimesValue)|null $callback + * @return static + */ + public static function times($number, ?callable $callback = null, ...$args) + { + if ($number < 1) { + return new static([], ...$args); + } + + return static::range(1, $number, 1, ...$args) + ->unless($callback == null) + ->map($callback); + } + + /** + * Create a new collection by decoding a JSON string. + * + * @param string $json + * @param int $depth + * @param int $flags + * @return static + */ + public static function fromJson($json, $depth = 512, $flags = 0, ...$args) + { + return new static(json_decode($json, true, $depth, $flags), ...$args); + } + + /** + * Get the average value of a given key. + * + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null + */ + public function avg($callback = null) + { + $callback = $this->valueRetriever($callback); + + $reduced = $this->reduce(static function (&$reduce, $value) use ($callback) { + if (! is_null($resolved = $callback($value))) { + $reduce[0] += $resolved; + $reduce[1]++; + } + + return $reduce; + }, [0, 0]); + + return $reduced[1] ? $reduced[0] / $reduced[1] : null; + } + + /** + * Alias for the "avg" method. + * + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null + */ + public function average($callback = null) + { + return $this->avg($callback); + } + + /** + * Alias for the "contains" method. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function some($key, $operator = null, $value = null) + { + return $this->contains(...func_get_args()); + } + + /** + * Dump the given arguments and terminate execution. + * + * @param mixed ...$args + * @return never + */ + public function dd(...$args) + { + dd($this->all(), ...$args); + } + + /** + * Dump the items. + * + * @param mixed ...$args + * @return $this + */ + public function dump(...$args) + { + dump($this->all(), ...$args); + + return $this; + } + + /** + * Execute a callback over each item. + * + * @param callable(TValue, TKey): mixed $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this as $key => $item) { + if ($callback($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * Execute a callback over each nested chunk of items. + * + * @param callable(...mixed): mixed $callback + * @return static + */ + public function eachSpread(callable $callback) + { + return $this->each(function ($chunk, $key) use ($callback) { + $chunk[] = $key; + + return $callback(...$chunk); + }); + } + + /** + * Determine if all items pass the given truth test. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function every($key, $operator = null, $value = null) + { + if (func_num_args() === 1) { + $callback = $this->valueRetriever($key); + + foreach ($this as $k => $v) { + if (! $callback($v, $k)) { + return false; + } + } + + return true; + } + + return $this->every($this->operatorForWhere(...func_get_args())); + } + + /** + * Get the first item by the given key value pair. + * + * @param callable|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue|null + */ + public function firstWhere($key, $operator = null, $value = null) + { + return $this->first($this->operatorForWhere(...func_get_args())); + } + + /** + * Determine if the collection contains multiple items, optionally matching the given criteria. + * + * @param (callable(TValue, TKey): bool)|string|null $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function hasMany($key = null, $operator = null, $value = null): bool + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->unless($filter == null) + ->filter($filter) + ->take(2) + ->count() === 2; + } + + /** + * Get a single key's value from the first matching item in the collection. + * + * @template TValueDefault + * + * @param string $key + * @param TValueDefault|(\Closure(): TValueDefault) $default + * @return TValue|TValueDefault + */ + public function value($key, $default = null) + { + $value = $this->first(function ($target) use ($key) { + return data_has($target, $key); + }); + + return data_get($value, $key, $default); + } + + /** + * Ensure that every item in the collection is of the expected type. + * + * @template TEnsureOfType + * + * @param class-string|array>|'string'|'int'|'float'|'bool'|'array'|'null' $type + * @return static + * + * @throws \UnexpectedValueException + */ + public function ensure($type) + { + $allowedTypes = is_array($type) ? $type : [$type]; + + return $this->each(function ($item, $index) use ($allowedTypes) { + $itemType = get_debug_type($item); + + foreach ($allowedTypes as $allowedType) { + if ($itemType === $allowedType || $item instanceof $allowedType) { + return true; + } + } + + throw new UnexpectedValueException( + sprintf("Collection should only include [%s] items, but '%s' found at position %d.", implode(', ', $allowedTypes), $itemType, $index) + ); + }); + } + + /** + * Determine if the collection is not empty. + * + * @phpstan-assert-if-true TValue $this->first() + * @phpstan-assert-if-true TValue $this->last() + * + * @phpstan-assert-if-false null $this->first() + * @phpstan-assert-if-false null $this->last() + * + * @return bool + */ + public function isNotEmpty() + { + return ! $this->isEmpty(); + } + + /** + * Run a map over each nested chunk of items. + * + * @template TMapSpreadValue + * + * @param callable(mixed...): TMapSpreadValue $callback + * @return static + */ + public function mapSpread(callable $callback) + { + return $this->map(function ($chunk, $key) use ($callback) { + $chunk[] = $key; + + return $callback(...$chunk); + }); + } + + /** + * Run a grouping map over the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapToGroupsKey of array-key + * @template TMapToGroupsValue + * + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToGroups(callable $callback) + { + $groups = $this->mapToDictionary($callback); + + return $groups->map($this->make(...)); + } + + /** + * Map a collection and flatten the result by a single level. + * + * @template TFlatMapKey of array-key + * @template TFlatMapValue + * + * @param callable(TValue, TKey): (\Illuminate\Support\Collection|array) $callback + * @return static + */ + public function flatMap(callable $callback) + { + return $this->map($callback)->collapse(); + } + + /** + * Map the values into a new class. + * + * @template TMapIntoValue + * + * @param class-string $class + * @return static + */ + public function mapInto($class) + { + if (is_subclass_of($class, BackedEnum::class)) { + return $this->map(fn ($value, $key) => $class::from($value)); + } + + return $this->map(fn ($value, $key) => new $class($value, $key)); + } + + /** + * Get the min value of a given key. + * + * @template TMinResult = mixed + * + * @param (callable(TValue): TMinResult)|string|null $callback + * @return ($callback is callable ? ?TMinResult : ($callback is null ? ?TValue : mixed)) + */ + public function min($callback = null) + { + $callback = $this->valueRetriever($callback); + + return $this->map(fn ($value) => $callback($value)) + ->reject(fn ($value) => is_null($value)) + ->reduce(fn ($result, $value) => is_null($result) || $value < $result ? $value : $result); + } + + /** + * Get the max value of a given key. + * + * @template TMaxResult = mixed + * + * @param (callable(TValue): TMaxResult)|string|null $callback + * @return ($callback is callable ? ?TMaxResult : ($callback is null ? ?TValue : mixed)) + */ + public function max($callback = null) + { + $callback = $this->valueRetriever($callback); + + return $this->reject(fn ($value) => is_null($value))->reduce(function ($result, $item) use ($callback) { + $value = $callback($item); + + return is_null($result) || $value > $result ? $value : $result; + }); + } + + /** + * "Paginate" the collection by slicing it into a smaller collection. + * + * @param int $page + * @param int $perPage + * @return static + */ + public function forPage($page, $perPage) + { + $offset = max(0, ($page - 1) * $perPage); + + return $this->slice($offset, $perPage); + } + + /** + * Partition the collection into two arrays using the given callback or key. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return static, static> + */ + public function partition($key, $operator = null, $value = null) + { + $callback = func_num_args() === 1 + ? $this->valueRetriever($key) + : $this->operatorForWhere(...func_get_args()); + + [$passed, $failed] = Arr::partition($this->getIterator(), $callback); + + return $this->newInstance([$this->newInstance($passed), $this->newInstance($failed)]); + } + + /** + * Calculate the percentage of items that pass a given truth test. + * + * @param (callable(TValue, TKey): bool) $callback + * @param int $precision + * @return float|null + */ + public function percentage(callable $callback, int $precision = 2) + { + if ($this->isEmpty()) { + return null; + } + + return round( + $this->filter($callback)->count() / $this->count() * 100, + $precision + ); + } + + /** + * Get the sum of the given values. + * + * @template TReturnType + * + * @param (callable(TValue, TKey): TReturnType)|string|null $callback + * @return ($callback is callable ? TReturnType : mixed) + */ + public function sum($callback = null) + { + $callback = is_null($callback) + ? $this->identity() + : $this->valueRetriever($callback); + + return $this->reduce(fn ($result, $item, $key) => $result + $callback($item, $key), 0); + } + + /** + * Apply the callback if the collection is empty. + * + * @template TWhenEmptyReturnType + * + * @param (callable($this): TWhenEmptyReturnType) $callback + * @param (callable($this): TWhenEmptyReturnType)|null $default + * @return $this|TWhenEmptyReturnType + */ + public function whenEmpty(callable $callback, ?callable $default = null) + { + return $this->when($this->isEmpty(), $callback, $default); + } + + /** + * Apply the callback if the collection is not empty. + * + * @template TWhenNotEmptyReturnType + * + * @param callable($this): TWhenNotEmptyReturnType $callback + * @param (callable($this): TWhenNotEmptyReturnType)|null $default + * @return $this|TWhenNotEmptyReturnType + */ + public function whenNotEmpty(callable $callback, ?callable $default = null) + { + return $this->when($this->isNotEmpty(), $callback, $default); + } + + /** + * Apply the callback unless the collection is empty. + * + * @template TUnlessEmptyReturnType + * + * @param callable($this): TUnlessEmptyReturnType $callback + * @param (callable($this): TUnlessEmptyReturnType)|null $default + * @return $this|TUnlessEmptyReturnType + */ + public function unlessEmpty(callable $callback, ?callable $default = null) + { + return $this->whenNotEmpty($callback, $default); + } + + /** + * Apply the callback unless the collection is not empty. + * + * @template TUnlessNotEmptyReturnType + * + * @param callable($this): TUnlessNotEmptyReturnType $callback + * @param (callable($this): TUnlessNotEmptyReturnType)|null $default + * @return $this|TUnlessNotEmptyReturnType + */ + public function unlessNotEmpty(callable $callback, ?callable $default = null) + { + return $this->whenEmpty($callback, $default); + } + + /** + * Filter items by the given key value pair. + * + * @param callable|string $key + * @param mixed $operator + * @param mixed $value + * @return static + */ + public function where($key, $operator = null, $value = null) + { + return $this->filter($this->operatorForWhere(...func_get_args())); + } + + /** + * Filter items where the value for the given key is null. + * + * @param string|null $key + * @return static + */ + public function whereNull($key = null) + { + return $this->whereStrict($key, null); + } + + /** + * Filter items where the value for the given key is not null. + * + * @param string|null $key + * @return static + */ + public function whereNotNull($key = null) + { + return $this->where($key, '!==', null); + } + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param mixed $value + * @return static + */ + public function whereStrict($key, $value) + { + return $this->where($key, '===', $value); + } + + /** + * Filter items by the given key value pair. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @param bool $strict + * @return static + */ + public function whereIn($key, $values, $strict = false) + { + $values = $this->getArrayableItems($values); + + return $this->filter(fn ($item) => in_array(data_get($item, $key), $values, $strict)); + } + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereInStrict($key, $values) + { + return $this->whereIn($key, $values, true); + } + + /** + * Filter items such that the value of the given key is between the given values. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereBetween($key, $values) + { + return $this->where($key, '>=', reset($values))->where($key, '<=', end($values)); + } + + /** + * Filter items such that the value of the given key is not between the given values. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereNotBetween($key, $values) + { + return $this->filter( + fn ($item) => data_get($item, $key) < reset($values) || data_get($item, $key) > end($values) + ); + } + + /** + * Filter items by the given key value pair. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @param bool $strict + * @return static + */ + public function whereNotIn($key, $values, $strict = false) + { + $values = $this->getArrayableItems($values); + + return $this->reject(fn ($item) => in_array(data_get($item, $key), $values, $strict)); + } + + /** + * Filter items by the given key value pair using strict comparison. + * + * @param string $key + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static + */ + public function whereNotInStrict($key, $values) + { + return $this->whereNotIn($key, $values, true); + } + + /** + * Filter the items, removing any items that don't match the given type(s). + * + * @template TWhereInstanceOf + * + * @param class-string|array> $type + * @return static + */ + public function whereInstanceOf($type) + { + return $this->filter(function ($value) use ($type) { + if (is_array($type)) { + return array_any($type, fn ($classType) => $value instanceof $classType); + } + + return $value instanceof $type; + }); + } + + /** + * Pass the collection to the given callback and return the result. + * + * @template TPipeReturnType + * + * @param callable($this): TPipeReturnType $callback + * @return TPipeReturnType + */ + public function pipe(callable $callback) + { + return $callback($this); + } + + /** + * Pass the collection into a new class. + * + * @template TPipeIntoValue + * + * @param class-string $class + * @return TPipeIntoValue + */ + public function pipeInto($class) + { + return new $class($this); + } + + /** + * Pass the collection through a series of callable pipes and return the result. + * + * @param array $callbacks + * @return mixed + */ + public function pipeThrough($callbacks) + { + return (new Collection($callbacks))->reduce( + fn ($carry, $callback) => $callback($carry), + $this, + ); + } + + /** + * Reduce the collection to a single value. + * + * @template TReduceInitial + * @template TReduceReturnType + * + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * @return TReduceInitial|TReduceReturnType + */ + public function reduce(callable $callback, $initial = null) + { + $result = $initial; + + foreach ($this as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + + /** + * Reduce the collection to a single value by mutating an initial value. + * + * @template TReduceIntoInitial + * + * @param TReduceIntoInitial $initial + * @param callable(TReduceIntoInitial, TValue, TKey): void $callback + * @return TReduceIntoInitial + */ + public function reduceInto($initial, callable $callback) + { + foreach ($this as $key => $value) { + $callback($initial, $value, $key); + } + + return $initial; + } + + /** + * Reduce the collection to multiple aggregate values. + * + * @param callable $callback + * @param mixed ...$initial + * @return array + * + * @throws \UnexpectedValueException + */ + public function reduceSpread(callable $callback, ...$initial) + { + $result = $initial; + + foreach ($this as $key => $value) { + $result = call_user_func_array($callback, array_merge($result, [$value, $key])); + + if (! is_array($result)) { + throw new UnexpectedValueException(sprintf( + "%s::reduceSpread expects reducer to return an array, but got a '%s' instead.", + class_basename(static::class), gettype($result) + )); + } + } + + return $result; + } + + /** + * Reduce an associative collection to a single value. + * + * @template TReduceWithKeysInitial + * @template TReduceWithKeysReturnType + * + * @param callable(TReduceWithKeysInitial|TReduceWithKeysReturnType, TValue, TKey): TReduceWithKeysReturnType $callback + * @param TReduceWithKeysInitial $initial + * @return TReduceWithKeysInitial|TReduceWithKeysReturnType + */ + public function reduceWithKeys(callable $callback, $initial = null) + { + return $this->reduce($callback, $initial); + } + + /** + * Create a collection of all elements that do not pass a given truth test. + * + * @param (callable(TValue, TKey): bool)|bool|TValue $callback + * @return static + */ + public function reject($callback = true) + { + $useAsCallable = $this->useAsCallable($callback); + + return $this->filter(function ($value, $key) use ($callback, $useAsCallable) { + return $useAsCallable + ? ! $callback($value, $key) + : $value != $callback; + }); + } + + /** + * Chunk the collection into chunks by comparing adjacent values using the given key or callback. + * + * @param (callable(TValue, TKey): mixed)|string $key + * @return static> + */ + public function chunkBy($key) + { + $callback = $this->valueRetriever($key); + + return $this->chunkWhile( + fn ($value, $key, $chunk) => $callback($value, $key) == $callback($chunk->last(), $chunk->keys()->last()) + ); + } + + /** + * Pass the collection to the given callback and then return it. + * + * @param callable($this): mixed $callback + * @return $this + */ + public function tap(callable $callback) + { + $callback($this); + + return $this; + } + + /** + * Return only unique items from the collection array. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @param bool $strict + * @return static + */ + public function unique($key = null, $strict = false) + { + $callback = $this->valueRetriever($key); + + $exists = []; + + return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) { + if (in_array($id = $callback($item, $key), $exists, $strict)) { + return true; + } + + $exists[] = $id; + }); + } + + /** + * Return only unique items from the collection array using strict comparison. + * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @return static + */ + public function uniqueStrict($key = null) + { + return $this->unique($key, true); + } + + /** + * Collect the values into a collection. + * + * @return \Illuminate\Support\Collection + */ + public function collect() + { + return new Collection($this->all()); + } + + /** + * Get the collection of items as a plain array. + * + * @return array + */ + public function toArray() + { + return $this->map(fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value)->all(); + } + + /** + * Convert the object into something JSON serializable. + * + * @return array + */ + public function jsonSerialize(): array + { + return array_map(function ($value) { + return match (true) { + $value instanceof JsonSerializable => $value->jsonSerialize(), + $value instanceof Jsonable => json_decode($value->toJson(), true), + $value instanceof Arrayable => $value->toArray(), + default => $value, + }; + }, $this->all()); + } + + /** + * Get the collection of items as JSON. + * + * @param int $options + * @return string + */ + public function toJson($options = 0) + { + return json_encode($this->jsonSerialize(), $options); + } + + /** + * Get the collection of items as pretty print formatted JSON. + * + * @param int $options + * @return string + */ + public function toPrettyJson(int $options = 0) + { + return $this->toJson(JSON_PRETTY_PRINT | $options); + } + + /** + * Get a CachingIterator instance. + * + * @param int $flags + * @return \CachingIterator + */ + public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING) + { + return new CachingIterator($this->getIterator(), $flags); + } + + /** + * Convert the collection to its string representation. + * + * @return string + */ + public function __toString() + { + return $this->escapeWhenCastingToString + ? e($this->toJson()) + : $this->toJson(); + } + + /** + * Indicate that the model's string representation should be escaped when __toString is invoked. + * + * @param bool $escape + * @return $this + */ + public function escapeWhenCastingToString($escape = true) + { + $this->escapeWhenCastingToString = $escape; + + return $this; + } + + /** + * Add a method to the list of proxied methods. + * + * @param string $method + * @return void + */ + public static function proxy($method) + { + static::$proxies[] = $method; + } + + /** + * Dynamically access collection proxies. + * + * @param string $key + * @return mixed + * + * @throws \Exception + */ + public function __get($key) + { + if (! in_array($key, static::$proxies)) { + throw new Exception("Property [{$key}] does not exist on this collection instance."); + } + + return new HigherOrderCollectionProxy($this, $key); + } + + /** + * Results array of items from Collection or Arrayable. + * + * @param mixed $items + * @return array + */ + protected function getArrayableItems($items) + { + return is_null($items) || is_scalar($items) || $items instanceof UnitEnum + ? Arr::wrap($items) + : Arr::from($items); + } + + /** + * Get an operator checker callback. + * + * @param callable|string $key + * @param string|null $operator + * @param mixed $value + * @return \Closure + */ + protected function operatorForWhere($key, $operator = null, $value = null) + { + if ($this->useAsCallable($key)) { + return $key; + } + + if (func_num_args() === 1) { + $value = true; + + $operator = '='; + } + + if (func_num_args() === 2) { + $value = $operator; + + $operator = '='; + } + + return function ($item) use ($key, $operator, $value) { + $retrieved = enum_value(data_get($item, $key)); + $value = enum_value($value); + + $strings = array_filter([$retrieved, $value], function ($value) { + return match (true) { + is_string($value) => true, + $value instanceof \Stringable => true, + default => false, + }; + }); + + if (count($strings) < 2 && count(array_filter([$retrieved, $value], 'is_object')) == 1) { + return in_array($operator, ['!=', '<>', '!==']); + } + + switch ($operator) { + default: + case '=': + case '==': return $retrieved == $value; + case '!=': + case '<>': return $retrieved != $value; + case '<': return $retrieved < $value; + case '>': return $retrieved > $value; + case '<=': return $retrieved <= $value; + case '>=': return $retrieved >= $value; + case '===': return $retrieved === $value; + case '!==': return $retrieved !== $value; + case '<=>': return $retrieved <=> $value; + } + }; + } + + /** + * Determine if the given value is callable, but not a string. + * + * @param mixed $value + * @return bool + */ + protected function useAsCallable($value) + { + return ! is_string($value) && is_callable($value); + } + + /** + * Get a value retrieving callback. + * + * @param callable|string|null $value + * @return callable + */ + protected function valueRetriever($value) + { + if ($this->useAsCallable($value)) { + return $value; + } + + return fn ($item) => data_get($item, $value); + } + + /** + * Make a function to check an item's equality. + * + * @param mixed $value + * @return \Closure(mixed): bool + */ + protected function equality($value) + { + return fn ($item) => $item === $value; + } + + /** + * Make a function using another function, by negating its result. + * + * @param \Closure $callback + * @return \Closure + */ + protected function negate(Closure $callback) + { + return fn (...$params) => ! $callback(...$params); + } + + /** + * Make a function that returns what's passed to it. + * + * @return \Closure(TValue): TValue + */ + protected function identity() + { + return fn ($value) => $value; + } +} diff --git a/src/Illuminate/Support/functions.php b/src/Illuminate/Support/functions.php new file mode 100644 index 00000000..6ccd9b3e --- /dev/null +++ b/src/Illuminate/Support/functions.php @@ -0,0 +1,27 @@ + $value->value, + $value instanceof \UnitEnum => $value->name, + + default => $value ?? value($default), + }; + } +} diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index b7e8eb36..164631d6 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -155,7 +155,7 @@ public function testExists(): void $this->assertTrue(Arr::exists([null], 0)); $this->assertTrue(Arr::exists(['a' => 1], 'a')); $this->assertTrue(Arr::exists(['a' => null], 'a')); - $this->assertTrue(Arr::exists(new Collection(['a' => null]), 'a')); + $this->assertFalse(Arr::exists(new Collection(['a' => null]), 'a')); $this->assertFalse(Arr::exists([1], 1)); $this->assertFalse(Arr::exists([null], 1)); @@ -1068,7 +1068,7 @@ public function testWhere(): void { $array = [100, '200', 300, '400', 500]; - $array = Arr::where($array, function ($key, $value) { + $array = Arr::where($array, function ($value, $key) { return is_string($value); }); @@ -1079,7 +1079,7 @@ public function testWhereKey(): void { $array = ['10' => 1, 'foo' => 3, 20 => 2]; - $array = Arr::where($array, function ($key, $value) { + $array = Arr::where($array, function ($value, $key) { return is_numeric($key); }); diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 62f4e328..3100ce36 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -64,20 +64,17 @@ public function testToArrayCallsToArrayOnEachItemInCollection(): void public function testToJsonEncodesTheToArrayResult(): void { - $c = $this->getMock(Collection::class, ['toArray']); - $c->expects($this->once())->method('toArray')->willReturn('foo'); - $results = $c->toJson(); + $c = new Collection(['foo' => 'bar']); - $this->assertEquals(json_encode('foo'), $results); + $this->assertEquals(json_encode(['foo' => 'bar']), $c->toJson()); } public function testCastingToStringJsonEncodesTheToArrayResult(): void { - $c = $this->getMock(\Illuminate\Database\Eloquent\Collection::class, ['toArray']); - $c->expects($this->once())->method('toArray')->willReturn('foo'); + $c = new Collection(['foo' => 'bar']); - $this->assertEquals(json_encode('foo'), (string) $c); + $this->assertEquals(json_encode(['foo' => 'bar']), (string) $c); } @@ -201,7 +198,7 @@ public function testCollapseWithNestedCollactions(): void public function testSort(): void { $data = new Collection([5, 3, 1, 2, 4]); - $data->sort(function($a, $b) + $data = $data->sort(function($a, $b) { if ($a === $b) { @@ -222,7 +219,7 @@ public function testSortBy(): void $this->assertEquals(['dayle', 'taylor'], array_values($data->all())); $data = new Collection(['dayle', 'taylor']); - $data->sortByDesc(function($x) { return $x; }); + $data = $data->sortByDesc(function($x) { return $x; }); $this->assertEquals(['taylor', 'dayle'], array_values($data->all())); } @@ -262,7 +259,7 @@ public function testChunk (): void $this->assertInstanceOf(Collection::class, $data[0]); $this->assertEquals(4, $data->count()); $this->assertEquals([1, 2, 3], $data[0]->toArray()); - $this->assertEquals([10], $data[3]->toArray()); + $this->assertEquals([9 => 10], $data[3]->toArray()); } @@ -305,9 +302,9 @@ public function testRandom(): void public function testRandomOnEmpty(): void { - $data = new Collection(); - $random = $data->random(); - $this->assertNull($random); + $this->expectException(\InvalidArgumentException::class); + + (new Collection())->random(); } @@ -315,15 +312,7 @@ public function testTakeLast(): void { $data = new Collection(['taylor', 'dayle', 'shawn']); $data = $data->take(-2); - $this->assertEquals(['dayle', 'shawn'], $data->all()); - } - - - public function testTakeAll(): void - { - $data = new Collection(['taylor', 'dayle', 'shawn']); - $data = $data->take(); - $this->assertEquals(['taylor', 'dayle', 'shawn'], $data->all()); + $this->assertEquals([1 => 'dayle', 2 => 'shawn'], $data->all()); } @@ -501,7 +490,7 @@ public function testRejectRemovesElementsPassingTruthTest(): void public function testKeys(): void { $c = new Collection(['name' => 'taylor', 'framework' => 'laravel']); - $this->assertEquals(['name', 'framework'], $c->keys()); + $this->assertEquals(['name', 'framework'], $c->keys()->all()); } }