.gitattributes000064400000000054151360551360007443 0ustar00/tests export-ignore /.github export-ignore LICENSE000064400000002110151360551360005550 0ustar00The MIT License (MIT) Copyright (c) Taylor Otwell Copyright (c) Hyperf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer.json000064400000002162151360551360007274 0ustar00{ "name": "hyperf/collection", "description": "Hyperf Collection package which come from illuminate/collections", "license": "MIT", "keywords": [ "php", "swoole", "hyperf", "collection" ], "homepage": "https://hyperf.io", "support": { "docs": "https://hyperf.wiki", "issues": "https://github.com/hyperf/hyperf/issues", "pull-request": "https://github.com/hyperf/hyperf/pulls", "source": "https://github.com/hyperf/hyperf" }, "require": { "php": ">=8.1", "hyperf/conditionable": "~3.1.0", "hyperf/contract": "~3.1.0", "hyperf/macroable": "~3.1.0", "hyperf/stringable": "~3.1.0" }, "autoload": { "psr-4": { "Hyperf\\Collection\\": "src/" }, "files": [ "src/Functions.php" ] }, "autoload-dev": { "psr-4": { "HyperfTest\\Collection\\": "tests/" } }, "config": { "sort-packages": true }, "extra": { "branch-alias": { "dev-master": "3.1-dev" } } } src/Arr.php000064400000054330151360551360006602 0ustar00all(); } elseif (! is_array($values)) { continue; } $results[] = $values; } return array_merge([], ...$results); } /** * Cross join the given arrays, returning all possible permutations. * * @param array ...$arrays */ public static function crossJoin(...$arrays): array { $results = [[]]; foreach ($arrays as $index => $array) { $append = []; foreach ($results as $product) { foreach ($array as $item) { $product[$index] = $item; $append[] = $product; } } $results = $append; } return $results; } /** * Divide an array into two arrays. One with keys and the other with values. */ public static function divide(array $array): array { return [array_keys($array), array_values($array)]; } /** * Flatten a multi-dimensional associative array with dots. */ public static function dot(array $array, string $prepend = ''): array { $results = []; foreach ($array as $key => $value) { if (is_array($value) && ! empty($value)) { $results = array_merge($results, static::dot($value, $prepend . $key . '.')); } else { $results[$prepend . $key] = $value; } } return $results; } /** * Get all the given array except for a specified array of keys. */ public static function except(array $array, array|int|string $keys): array { static::forget($array, $keys); return $array; } /** * Determine if the given key exists in the provided array. */ public static function exists(array|ArrayAccess $array, int|string $key): bool { if ($array instanceof ArrayAccess) { return $array->offsetExists($key); } return array_key_exists($key, $array); } /** * Return the first element in an array passing a given truth test. */ public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed { if (is_null($callback)) { if (empty($array)) { return value($default); } foreach ($array as $item) { return $item; } } foreach ($array as $key => $value) { if (call_user_func($callback, $value, $key)) { return $value; } } return value($default); } /** * Return the last element in an array passing a given truth test. */ public static function last(array $array, ?callable $callback = null, mixed $default = null): mixed { if (is_null($callback)) { return empty($array) ? value($default) : end($array); } return static::first(array_reverse($array, true), $callback, $default); } /** * Flatten a multi-dimensional array into a single level. */ public static function flatten(array $array, float|int $depth = INF): array { $result = []; foreach ($array as $item) { $item = $item instanceof Collection ? $item->all() : $item; if (! is_array($item)) { $result[] = $item; } else { $values = $depth <= 1 ? array_values($item) : static::flatten($item, $depth - 1); foreach ($values as $value) { $result[] = $value; } } } return $result; } /** * Remove one or many array items from a given array using "dot" notation. * * @param array|string $keys */ public static function forget(array &$array, array|int|string $keys): void { $original = &$array; $keys = (array) $keys; if (count($keys) === 0) { return; } foreach ($keys as $key) { // if the exact key exists in the top-level, remove it if (static::exists($array, $key)) { unset($array[$key]); continue; } $parts = explode('.', (string) $key); // clean up before each pass $array = &$original; while (count($parts) > 1) { $part = array_shift($parts); if (isset($array[$part]) && is_array($array[$part])) { $array = &$array[$part]; } else { continue 2; } } unset($array[array_shift($parts)]); } } /** * Get an item from an array using "dot" notation. */ public static function get(mixed $array, null|int|string $key = null, mixed $default = null) { if (! static::accessible($array)) { return value($default); } if (is_null($key)) { return $array; } if (static::exists($array, $key)) { return $array[$key]; } if (! is_string($key) || ! str_contains($key, '.')) { return $array[$key] ?? value($default); } foreach (explode('.', $key) as $segment) { if (static::accessible($array) && static::exists($array, $segment)) { $array = $array[$segment]; } else { return value($default); } } return $array; } /** * Check if an item or items exist in an array using "dot" notation. * * @param null|array|string $keys */ public static function has(array|ArrayAccess $array, null|array|int|string $keys): bool { if (is_null($keys)) { return false; } $keys = (array) $keys; if (! $array) { return false; } if ($keys === []) { return false; } foreach ($keys as $key) { $subKeyArray = $array; if (static::exists($array, $key)) { continue; } foreach (explode('.', (string) $key) as $segment) { if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { $subKeyArray = $subKeyArray[$segment]; } else { return false; } } } return true; } /** * Determine if any of the keys exist in an array using "dot" notation. */ public static function hasAny(array|ArrayAccess $array, null|array|int|string $keys): bool { if (is_null($keys)) { return false; } $keys = (array) $keys; if (! $array) { return false; } if ($keys === []) { return false; } foreach ($keys as $key) { if (static::has($array, $key)) { return true; } } return false; } /** * Determines if an array is associative. * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. */ public static function isAssoc(array $array): bool { $keys = array_keys($array); return array_keys($keys) !== $keys; } /** * Determines if an array is a list. * * An array is a "list" if all array keys are sequential integers starting from 0 with no gaps in between. */ public static function isList(array $array): bool { return array_is_list($array); } /** * 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 array $array * @param callable(TValue, TKey): array $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; } /** * Get a subset of the items from the given array. */ public static function only(array $array, array|int|string $keys): array { return array_intersect_key($array, array_flip((array) $keys)); } /** * Pluck an array of values from an array. */ public static function pluck(array $array, array|string $value, null|array|string $key = null): array { $results = []; [$value, $key] = static::explodePluckParameters($value, $key); foreach ($array as $item) { $itemValue = data_get($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 = data_get($item, $key); if (is_object($itemKey) && method_exists($itemKey, '__toString')) { $itemKey = (string) $itemKey; } $results[$itemKey] = $itemValue; } } return $results; } /** * Push an item onto the beginning of an array. * * @param array $array * @param null|TKey $key * @param TValue $value * @return array */ public static function prepend(array $array, mixed $value, null|int|string $key = null): array { if (is_null($key)) { array_unshift($array, $value); } else { $array = [$key => $value] + $array; } return $array; } /** * Get a value from the array, and remove it. */ public static function pull(array &$array, string $key, mixed $default = null): mixed { $value = static::get($array, $key, $default); static::forget($array, $key); return $value; } /** * Get one or a specified number of random values from an array. * * @throws InvalidArgumentException */ public static function random(array $array, ?int $number = null): mixed { $requested = is_null($number) ? 1 : $number; $count = count($array); if ($requested > $count) { throw new InvalidArgumentException("You requested {$requested} items, but there are only {$count} items available."); } if (is_null($number)) { return $array[array_rand($array)]; } if ($number === 0) { return []; } $keys = array_rand($array, $number); $results = []; foreach ((array) $keys as $key) { $results[] = $array[$key]; } return $results; } /** * Set an array item to a given value using "dot" notation. * If no key is given to the method, the entire array will be replaced. */ public static function set(array &$array, null|int|string $key, mixed $value): array { if (is_null($key)) { return $array = $value; } if (! is_string($key)) { $array[$key] = $value; return $array; } $keys = explode('.', $key); while (count($keys) > 1) { $key = array_shift($keys); // If the key doesn't exist at this depth, we will just create an empty array // to hold the next value, allowing us to create the arrays to hold final // values at the correct depth. Then we'll keep digging into the array. if (! isset($array[$key]) || ! is_array($array[$key])) { $array[$key] = []; } $array = &$array[$key]; } $array[array_shift($keys)] = $value; return $array; } /** * Shuffle the given array and return the result. */ public static function shuffle(array $array, ?int $seed = null): array { if (empty($array)) { return []; } if (! is_null($seed)) { mt_srand($seed); shuffle($array); mt_srand(); return $array; } shuffle($array); return $array; } /** * Sort the array using the given callback or "dot" notation. */ public static function sort(array $array, null|callable|string $callback = null): array { return Collection::make($array)->sortBy($callback)->all(); } /** * Recursively sort an array by keys and values. */ public static function sortRecursive(array $array, int $options = SORT_REGULAR, bool $descending = false): array { foreach ($array as &$value) { if (is_array($value)) { $value = static::sortRecursive($value, $options, $descending); } } if (static::isAssoc($array)) { $descending ? krsort($array, $options) : ksort($array, $options); } else { $descending ? rsort($array, $options) : sort($array, $options); } return $array; } /** * Convert the array into a query string. */ public static function query(array $array): string { return http_build_query($array, '', '&', PHP_QUERY_RFC3986); } /** * Filter the array using the given callback. */ public static function where(array $array, callable $callback): array { return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); } /** * If the given value is not an array and not null, wrap it in one. * @param mixed $value */ public static function wrap($value): array { if (is_null($value)) { return []; } return ! is_array($value) ? [$value] : $value; } /** * Make array elements unique. */ public static function unique(array $array): array { $result = []; foreach ($array as $key => $item) { if (is_array($item)) { $result[$key] = self::unique($item); } else { $result[$key] = $item; } } if (! self::isAssoc($result)) { return array_unique($result); } return $result; } public static function merge(array $array1, array $array2, bool $unique = true): array { $isAssoc = static::isAssoc($array1 ?: $array2); if ($isAssoc) { foreach ($array2 as $key => $value) { if (is_array($value)) { $array1[$key] = static::merge($array1[$key] ?? [], $value, $unique); } else { $array1[$key] = $value; } } } else { foreach ($array2 as $value) { if ($unique && in_array($value, $array1, true)) { continue; } $array1[] = $value; } $array1 = array_values($array1); } return $array1; } /** * Remove one or more elements from an array. */ public static function remove(array $array, mixed ...$value): array { $array = array_diff($array, $value); return array_values($array); } /** * Removes one or more elements from an array, keeping the original keys. */ public static function removeKeepKey(array $array, mixed ...$value): array { foreach ($value as $item) { while (false !== ($index = array_search($item, $array))) { unset($array[$index]); } } return $array; } /** * Convert a flatten "dot" notation array into an expanded array. */ public static function undot(array $array): array { $result = []; foreach ($array as $key => $value) { static::set($result, $key, $value); } return $result; } /** * Conditionally compile classes from an array into a CSS class list. */ public static function toCssClasses(array $array): string { $classList = static::wrap($array); $classes = []; foreach ($classList as $class => $constraint) { if (is_numeric($class)) { $classes[] = $constraint; } elseif ($constraint) { $classes[] = $class; } } return implode(' ', $classes); } /** * Conditionally compile styles from an array into a style list. */ public static function toCssStyles(array $array): string { $styleList = static::wrap($array); $styles = []; foreach ($styleList as $class => $constraint) { if (is_numeric($class)) { $styles[] = Str::finish($constraint, ';'); } elseif ($constraint) { $styles[] = Str::finish($class, ';'); } } return implode(' ', $styles); } /** * Join all items using a string. The final items can use a separate glue string. */ public static function join(array $array, string $glue, string $finalGlue = ''): string { if ($finalGlue === '') { return implode($glue, $array); } if (count($array) === 0) { return ''; } if (count($array) === 1) { return end($array); } $finalItem = array_pop($array); return implode($glue, $array) . $finalGlue . $finalItem; } /** * Key an associative array by a field or using a callback. */ public static function keyBy(array $array, array|callable|string $keyBy): array { return Collection::make($array)->keyBy($keyBy)->all(); } /** * Prepend the key names of an associative array. */ public static function prependKeysWith(array $array, string $prependWith): array { return static::mapWithKeys($array, fn ($item, $key) => [$prependWith . $key => $item]); } /** * Select an array of values from an array. */ public static function select(array $array, array|string $keys): array { $keys = static::wrap($keys); return static::map($array, static 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; }); } /** * Run a map over each nested chunk of items. * * @template TMapSpreadValue * * @param callable(mixed...): TMapSpreadValue $callback * @return array */ public static function mapSpread(array $array, callable $callback): array { return static::map($array, function ($chunk, $key) use ($callback) { $chunk[] = $key; return $callback(...$chunk); }); } /** * Run a map over each of the items in the array. */ public static function map(array $array, callable $callback): array { $keys = array_keys($array); try { $items = array_map($callback, $array, $keys); } catch (ArgumentCountError) { $items = array_map($callback, $array); } return array_combine($keys, $items); } /** * Sort the array in descending order using the given callback or "dot" notation. */ public static function sortDesc(array $array, null|array|callable|string $callback = null): array { return Collection::make($array)->sortByDesc($callback)->all(); } /** * Recursively sort an array by keys and values in descending order. */ public static function sortRecursiveDesc(array $array, int $options = SORT_REGULAR): array { return static::sortRecursive($array, $options, true); } /** * Filter items where the value is not null. */ public static function whereNotNull(array $array): array { return static::where($array, static fn ($value) => ! is_null($value)); } /** * Explode the "value" and "key" arguments passed to "pluck". */ protected static function explodePluckParameters(array|string $value, null|array|string $key): array { $value = is_string($value) ? explode('.', $value) : $value; $key = is_null($key) || is_array($key) ? $key : explode('.', $key); return [$value, $key]; } } src/Collection.php000064400000130452151360551360010151 0ustar00 * @implements Enumerable * * @property HigherOrderCollectionProxy $average * @property HigherOrderCollectionProxy $avg * @property HigherOrderCollectionProxy $contains * @property HigherOrderCollectionProxy $each * @property HigherOrderCollectionProxy $every * @property HigherOrderCollectionProxy $filter * @property HigherOrderCollectionProxy $first * @property HigherOrderCollectionProxy $flatMap * @property HigherOrderCollectionProxy $groupBy * @property HigherOrderCollectionProxy $keyBy * @property HigherOrderCollectionProxy $map * @property HigherOrderCollectionProxy $max * @property HigherOrderCollectionProxy $min * @property HigherOrderCollectionProxy $partition * @property HigherOrderCollectionProxy $reject * @property HigherOrderCollectionProxy $sortBy * @property HigherOrderCollectionProxy $sortByDesc * @property HigherOrderCollectionProxy $sum * @property HigherOrderCollectionProxy $unique */ class Collection implements Enumerable, ArrayAccess { use EnumeratesValues; use Macroable; /** * The items contained in the collection. * * @var array */ protected array $items = []; /** * Create a new collection. * @param null|iterable|Jsonable|JsonSerializable $items */ public function __construct($items = []) { $this->items = $this->getArrayableItems($items); } /** * @param null|iterable|Jsonable|JsonSerializable $items * @return static */ public function fill($items = []) { $this->items = $this->getArrayableItems($items); return $this; } /** * Get all of the items in the collection. * * @return array */ public function all(): array { return $this->items; } /** * Get the median of a given key. * * @param null|array|string $key * @return null|float|int */ public function median($key = null) { $values = (isset($key) ? $this->pluck($key) : $this)->filter(function ($item) { return ! is_null($item); })->sort()->values(); $count = $values->count(); if ($count == 0) { return null; } $middle = (int) ($count / 2); if ($count % 2) { return $values->get($middle); } return (new static([ $values->get($middle - 1), $values->get($middle), ]))->average(); } /** * Get the mode of a given key. * * @param null|array|string $key * @return null|array */ public function mode($key = null) { if ($this->count() == 0) { return null; } $collection = isset($key) ? $this->pluck($key) : $this; /** * @template TValue of array-key * @phpstan-ignore-next-line * @var static $counts */ $counts = new self(); $collection->each(function ($value) use ($counts) { $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1; }); $sorted = $counts->sort(); $highestValue = $sorted->last(); return $sorted->filter(function ($value) use ($highestValue) { return $value == $highestValue; })->sort()->keys()->all(); } /** * Collapse the collection of items into a single array. * * @return static */ public function collapse(): self { return new static(Arr::collapse($this->items)); } /** * Determine if an item exists in the collection. * * @param null|mixed $operator * @param null|mixed $value * @param (callable(TValue): bool)|string|TValue $key */ public function contains($key, $operator = null, $value = null): bool { if (func_num_args() === 1) { if ($this->useAsCallable($key)) { $placeholder = new stdClass(); return $this->first($key, $placeholder) !== $placeholder; } return in_array($key, $this->items); } return $this->contains($this->operatorForWhere(...func_get_args())); } /** * Determine if the collection contains a single item. */ public function containsOneItem(): bool { return $this->count() === 1; } /** * Determine if an item exists in the collection using strict comparison. * * @param null|TValue $value * @param callable|TKey|TValue $key */ public function containsStrict($key, $value = null): bool { if (func_num_args() === 2) { return $this->contains(function ($item) use ($key, $value) { return data_get($item, $key) === $value; }); } if ($this->useAsCallable($key)) { return ! is_null($this->first($key)); } return in_array($key, $this->items, true); } /** * Cross join with the given lists, returning all possible permutations. */ public function crossJoin(...$lists): static { return new static(Arr::crossJoin($this->items, ...array_map([$this, 'getArrayableItems'], $lists))); } /** * Determine if an item is not contained in the collection. * * @param null|mixed $operator * @param null|mixed $value * @param (callable(TValue): bool)|string|TValue $key */ public function doesntContain($key, $operator = null, $value = null): bool { return ! $this->contains(...func_get_args()); } /** * Flatten a multi-dimensional associative array with dots. * * @return static */ public function dot(): static { return new static(Arr::dot($this->all())); } /** * Convert a flatten "dot" notation array into an expanded array. */ public function undot(): static { return new static(Arr::undot($this->all())); } /** * Get the items in the collection that are not present in the given items. * * @param Arrayable|iterable $items * @return static */ public function diff($items): static { return new static(array_diff($this->items, $this->getArrayableItems($items))); } /** * Get the items in the collection that are not present in the given items. * * @param Arrayable|iterable $items * @param callable(TValue): int $callback * @return static */ public function diffUsing($items, callable $callback): static { return new static(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 Arrayable|iterable $items * @return static */ public function diffAssoc($items): static { return new static(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. * * @param Arrayable|iterable $items * @param callable(TKey): int $callback * @return static */ public function diffAssocUsing($items, callable $callback): static { return new static(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 Arrayable|iterable $items * @return static */ public function diffKeys($items): static { return new static(array_diff_key($this->items, $this->getArrayableItems($items))); } /** * Get the items in the collection whose keys are not present in the given items. * * @param Arrayable|iterable $items * @param callable(TKey): int $callback * @return static */ public function diffKeysUsing($items, callable $callback): static { return new static(array_diff_ukey($this->items, $this->getArrayableItems($items), $callback)); } /** * Get all items except for those with the specified keys. * * @param null|array|static $keys * @return static */ public function except($keys): static { if (is_null($keys)) { return new static($this->items); } if ($keys instanceof self) { $keys = $keys->all(); } elseif (! is_array($keys)) { $keys = func_get_args(); } return new static(Arr::except($this->items, $keys)); } /** * Run a filter over each of the items. * * @param null|(callable(TValue, TKey): bool) $callback * @return static */ public function filter(?callable $callback = null): static { if ($callback) { return new static(Arr::where($this->items, $callback)); } return new static(array_filter($this->items)); } /** * Get the first item from the collection. * * @template TFirstDefault * * @param null|(callable(TValue, TKey): bool) $callback * @param (Closure(): TFirstDefault)|TFirstDefault $default * @return TFirstDefault|TValue */ public function first(?callable $callback = null, $default = null) { return Arr::first($this->items, $callback, $default); } /** * Get the first item by the given key value pair. * * @return null|TValue */ public function firstWhere(callable|string $key, mixed $operator = null, mixed $value = null): mixed { return $this->first($this->operatorForWhere(...func_get_args())); } /** * Get a flattened array of the items in the collection. * * @param float|int $depth * @return static */ public function flatten($depth = INF): self { return new static(Arr::flatten($this->items, $depth)); } /** * Flip the items in the collection. * * @return static */ public function flip(): self|static { return new static(array_flip($this->items)); } /** * Remove an item from the collection by key. * * @param \Hyperf\Contract\Arrayable|iterable|TKey $keys * @return $this */ public function forget($keys): static { foreach ($this->getArrayableItems($keys) as $key) { $this->offsetUnset($key); } return $this; } /** * Get an item from the collection by key. * * @template TGetDefault * * @param TKey $key * @param (Closure(): TGetDefault)|TGetDefault $default * @return TGetDefault|TValue */ public function get($key, $default = null) { if ($this->offsetExists($key)) { 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 (Closure(): TGetOrPutValue)|TGetOrPutValue $value * @return TGetOrPutValue|TValue */ public function getOrPut(int|string $key, mixed $value): mixed { if (array_key_exists($key, $this->items)) { return $this->items[$key]; } $this->offsetSet($key, $value = value($value)); return $value; } /** * Get an item from the collection by key or add it to collection if it does not exist. * * @template TGetOrPutValue * * @param (Closure(): TGetOrPutValue)|TGetOrPutValue $value * @return TGetOrPutValue|TValue */ public function getOrSet(int|string $key, mixed $value): mixed { return $this->getOrPut($key, $value); } /** * Group an associative array by a field or using a callback. * @param mixed $groupBy */ public function groupBy($groupBy, bool $preserveKeys = false): static { if (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 = is_bool($groupKey) ? (int) $groupKey : $groupKey; if (! array_key_exists($groupKey, $results)) { $results[$groupKey] = new static(); } $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value); } } $result = new static($results); if (! empty($nextGroups)) { return $result->map->groupBy($nextGroups, $preserveKeys); } return $result; } /** * Key an associative array by a field or using a callback. * * @param array|(callable(TValue, TKey): array-key)|string $keyBy * @return static */ public function keyBy($keyBy): static { $keyBy = $this->valueRetriever($keyBy); $results = []; foreach ($this->items as $key => $item) { $resolvedKey = $keyBy($item, $key); if (is_object($resolvedKey)) { $resolvedKey = (string) $resolvedKey; } $results[$resolvedKey] = $item; } return new static($results); } /** * Determine if an item exists in the collection by key. * @param array|TKey $key */ public function has($key): bool { $keys = is_array($key) ? $key : func_get_args(); foreach ($keys as $value) { if (! $this->offsetExists($value)) { return false; } } return true; } /** * Determine if any of the keys exist in the collection. * * @param array|TKey $key */ public function hasAny($key): bool { if ($this->isEmpty()) { return false; } $keys = is_array($key) ? $key : func_get_args(); foreach ($keys as $value) { if ($this->has($value)) { return true; } } return false; } /** * Concatenate values of a given key as a string. */ public function implode(array|callable|string $value, ?string $glue = null): string { 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 Arrayable|iterable $items * @return static */ public function intersect(mixed $items): static { return new static(array_intersect($this->items, $this->getArrayableItems($items))); } /** * Intersect the collection with the given items with additional index check. * * @param Arrayable|iterable $items * @return static */ public function intersectAssoc($items): static { return new static(array_intersect_assoc($this->items, $this->getArrayableItems($items))); } /** * Intersect the collection with the given items by key. * @param Arrayable|iterable $items * @return static */ public function intersectByKeys($items): static { return new static(array_intersect_key($this->items, $this->getArrayableItems($items))); } /** * Determine if the collection is empty or not. */ public function isEmpty(): bool { return empty($this->items); } /** * Get the keys of the collection items. * @return static */ public function keys(): self|static { return new static(array_keys($this->items)); } /** * Get the last item from the collection. * * @template TLastDefault * * @param null|(callable(TValue, TKey): bool) $callback * @param (Closure(): TLastDefault)|TLastDefault $default * @return TLastDefault|TValue */ public function last(?callable $callback = null, $default = null) { return Arr::last($this->items, $callback, $default); } /** * Get the values of a given key. * * @param array|string $value * @return static */ public function pluck(array|string $value, ?string $key = null): self|static { return new static(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): self|static { $result = []; foreach ($this->items as $key => $value) { $result[$key] = $callback($value, $key); } return new static($result); } /** * 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): static { $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 new static($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): static { return new static(Arr::mapWithKeys($this->items, $callback)); } /** * Merge the collection with the given items. * @param Arrayable|iterable $items * @return static */ public function merge($items): static { return new static(array_merge($this->items, $this->getArrayableItems($items))); } /** * Recursively merge the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function mergeRecursive($items): static { return new static(array_merge_recursive($this->items, $this->getArrayableItems($items))); } /** * Create a collection by using this collection for keys and another for its values. * * @template TCombineValue * * @param Arrayable|iterable $values * @return static */ public function combine($values): static { return new static(array_combine($this->all(), $this->getArrayableItems($values))); } /** * Union the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function union($items): static { return new static($this->items + $this->getArrayableItems($items)); } /** * Create a new collection consisting of every n-th element. * * @return static */ public function nth(int $step, int $offset = 0): static { $new = []; $position = 0; foreach ($this->items as $item) { if ($position % $step === $offset) { $new[] = $item; } ++$position; } return new static($new); } /** * Get the items with the specified keys. * * @param null|array|static|string $keys * @return static */ public function only($keys): static { if (is_null($keys)) { return new static($this->items); } if ($keys instanceof self) { $keys = $keys->all(); } $keys = is_array($keys) ? $keys : func_get_args(); return new static(Arr::only($this->items, $keys)); } /** * Get and remove the last item from the collection. */ public function pop() { return array_pop($this->items); } /** * Push an item onto the beginning of the collection. * * @param TValue $value * @param null|TKey $key * @return $this */ public function prepend($value, $key = null): static { $this->items = Arr::prepend($this->items, $value, $key); return $this; } /** * Push an item onto the end of the collection. * * @param TValue $value * @return $this */ public function push($value): static { $this->offsetSet(null, $value); return $this; } /** * Push all of the given items onto the collection. * * @param iterable $source * @return static */ public function concat($source): static { $result = new static($this); foreach ($source as $item) { $result->push($item); } return $result; } /** * Get and remove an item from the collection. * * @template TPullDefault * * @param TKey $key * @param (Closure(): TPullDefault)|TPullDefault $default * @return TPullDefault|TValue */ 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): static { $this->offsetSet($key, $value); return $this; } /** * Get one or a specified number of items randomly from the collection. * * @return static|TValue * @throws InvalidArgumentException */ public function random(?int $number = null) { if (is_null($number)) { return Arr::random($this->items); } return new static(Arr::random($this->items, $number)); } /** * Create a collection with the given range. * * @return static */ public static function range(float|int|string $from, float|int|string $to): static { return new static(range($from, $to)); } /** * Replace the collection items with the given items. * * @param Arrayable|iterable $items * @return static */ public function replace($items) { return new static(array_replace($this->items, $this->getArrayableItems($items))); } /** * Recursively replace the collection items with the given items. * * @param Arrayable|iterable $items * @return static */ public function replaceRecursive($items) { return new static(array_replace_recursive($this->items, $this->getArrayableItems($items))); } /** * Reverse items order. * * @return static */ public function reverse(): static { return new static(array_reverse($this->items, true)); } /** * Search the collection for a given value and return the corresponding key if successful. * * @param (callable(TValue,TKey): bool)|TValue $value * @return bool|TKey */ public function search($value, bool $strict = false) { if (! $this->useAsCallable($value)) { return array_search($value, $this->items, $strict); } foreach ($this->items as $key => $item) { if (call_user_func($value, $item, $key)) { return $key; } } return false; } /** * Get and remove the first item from the collection. * * @return null|TValue */ public function shift() { return array_shift($this->items); } /** * Shuffle the items in the collection. * * @return static */ public function shuffle(?int $seed = null): static { return new static(Arr::shuffle($this->items, $seed)); } /** * Skip the first {$count} items. * * @return static */ public function skip(int $count): static { return $this->slice($count); } /** * Slice the underlying collection array. * * @return static */ public function slice(int $offset, ?int $length = null): static { return new static(array_slice($this->items, $offset, $length, true)); } /** * Create chunks representing a "sliding window" view of the items in the collection. * * @return static */ public function sliding(int $size = 2, int $step = 1): static { $chunks = (int) floor(($this->count() - $size) / $step) + 1; return static::times($chunks, fn ($number) => $this->slice(($number - 1) * $step, $size)); } /** * Split a collection into a certain number of groups. * * @return static> */ public function split(int $numberOfGroups): static { if ($this->isEmpty()) { return new static(); } $groups = new static(); $groupSize = (int) 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(new static(array_slice($this->items, $start, $size))); $start += $size; } } return $groups; } /** * Chunk the underlying collection array. * * @return static> */ public function chunk(int $size): static { if ($size <= 0) { return new static(); } $chunks = []; foreach (array_chunk($this->items, $size, true) as $chunk) { $chunks[] = new static($chunk); } return new static($chunks); } /** * Sort through each item with a callback. * * @param callable(TValue, TValue): int $callback * @return static */ public function sort(?callable $callback = null): static { $items = $this->items; $callback ? uasort($items, $callback) : asort($items); return new static($items); } /** * Sort the collection using the given callback. * * @param array|(callable(TValue, TKey): mixed)|string $callback * @return static */ public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): static { 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]; } return new static($results); } /** * Sort the collection in descending order using the given callback. * * @param (callable(TValue, TKey): mixed)|string $callback * @return static */ public function sortByDesc($callback, int $options = SORT_REGULAR): static { return $this->sortBy($callback, $options, true); } /** * Sort items in descending order. * * @return static */ public function sortDesc(int $options = SORT_REGULAR): static { $items = $this->items; arsort($items, $options); return new static($items); } /** * Sort the collection keys. * * @return static */ public function sortKeys(int $options = SORT_REGULAR, bool $descending = false): static { $items = $this->items; $descending ? krsort($items, $options) : ksort($items, $options); return new static($items); } /** * Sort the collection keys in descending order. * * @return static */ public function sortKeysDesc(int $options = SORT_REGULAR): static { return $this->sortKeys($options, true); } /** * Sort the collection keys using a callback. * * @param callable(TKey, TKey): int $callback * @return static */ public function sortKeysUsing(callable $callback): static { $items = $this->items; uksort($items, $callback); return new static($items); } /** * Splice a portion of the underlying collection array. * * @param array $replacement * @return static */ public function splice(int $offset, ?int $length = null, $replacement = []): static { if (func_num_args() === 1) { return new static(array_splice($this->items, $offset)); } return new static(array_splice($this->items, $offset, $length, $replacement)); } /** * Split a collection into a certain number of groups, and fill the first groups completely. * * @return static> */ public function splitIn(int $numberOfGroups) { return $this->chunk((int) ceil($this->count() / $numberOfGroups)); } /** * Take the first or last {$limit} items. * * @return static */ public function take(int $limit): static { if ($limit < 0) { return $this->slice($limit, abs($limit)); } return $this->slice(0, $limit); } /** * Transform each item in the collection using a callback. * * @param callable(TValue, TKey): TValue $callback * @return $this */ public function transform(callable $callback): static { $this->items = $this->map($callback)->all(); 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; } /** * Reset the keys on the underlying array. * * @return static */ public function values(): static { return new static(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 Arrayable|iterable ...$items * @return static> */ public function zip($items): self|static { $arrayableItems = array_map(function ($items) { return $this->getArrayableItems($items); }, func_get_args()); $params = array_merge([ function () { return new static(func_get_args()); }, $this->items, ], $arrayableItems); return new static(call_user_func_array('array_map', $params)); } /** * Pad collection to the specified length with a value. * * @template TPadValue * * @param TPadValue $value * @return static */ public function pad(int $size, $value): self|static { return new static(array_pad($this->items, $size, $value)); } /** * Retrieve duplicate items from the collection. * * @param null|(callable(TValue): bool)|string $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 = new static(); foreach ($items as $key => $value) { if ($uniqueItems->isNotEmpty() && $compare($value, $uniqueItems->first())) { $uniqueItems->shift(); } else { $duplicates[$key] = $value; } } return $duplicates; } /** * 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 ItemNotFoundException * @throws 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(); } /** * Get an iterator for the items. * * @return ArrayIterator */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->items); } /** * Count the number of items in the collection. */ public function count(): int { return count($this->items); } /** * Get a base Support collection instance from this collection. * * @return Collection */ public function toBase() { return new self($this); } /** * Determine if an item exists at an offset. * * @param TKey $offset */ public function offsetExists(mixed $offset): bool { return isset($this->items[$offset]); } /** * Get an item at a given offset. * * @param TKey $offset * @return TValue */ public function offsetGet(mixed $offset): mixed { return $this->items[$offset]; } /** * Set the item at a given offset. * * @param null|TKey $offset * @param TValue $value */ public function offsetSet(mixed $offset, mixed $value): void { if (is_null($offset)) { $this->items[] = $value; } else { $this->items[$offset] = $value; } } /** * Unset the item at a given offset. * * @param TKey $offset */ public function offsetUnset(mixed $offset): void { unset($this->items[$offset]); } /** * 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 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; } /** * 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 = '') { if ($finalGlue === '') { return $this->implode($glue); } $count = $this->count(); if ($count === 0) { return ''; } if ($count === 1) { return $this->last(); } $collection = new static($this->items); $finalItem = $collection->pop(); return $collection->implode($glue) . $finalGlue . $finalItem; } /** * Intersect the collection with the given items with additional index check, using the callback. * * @param Arrayable|iterable $items * @param callable(TValue, TValue): int $callback * @return static */ public function intersectAssocUsing($items, callable $callback) { return new static(array_intersect_uassoc($this->items, $this->getArrayableItems($items), $callback)); } /** * Intersect the collection with the given items, using the callback. * * @param Arrayable|iterable $items * @param callable(TValue, TValue): int $callback * @return static */ public function intersectUsing($items, callable $callback) { return new static(array_uintersect($this->items, $this->getArrayableItems($items), $callback)); } /** * Retrieve duplicate items from the collection using strict comparison. * * @param null|(callable(TValue): bool)|string $callback * @return static */ public function duplicatesStrict($callback = null) { return $this->duplicates($callback, true); } /** * Get a lazy collection for the items in this collection. * * @return LazyCollection */ public function lazy() { return new LazyCollection($this->items); } /** * Skip items in the collection until the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function skipUntil($value) { return new static($this->lazy()->skipUntil($value)->all()); } /** * Skip items in the collection while the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function skipWhile($value) { return new static($this->lazy()->skipWhile($value)->all()); } /** * Chunk the collection into chunks with a callback. * * @param callable(TValue, TKey, static): bool $callback * @return static> */ public function chunkWhile(callable $callback) { return new static( $this->lazy()->chunkWhile($callback)->mapInto(static::class) ); } /** * Take items in the collection until the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function takeUntil($value) { return new static($this->lazy()->takeUntil($value)->all()); } /** * Take items in the collection while the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function takeWhile($value) { return new static($this->lazy()->takeWhile($value)->all()); } /** * Count the number of items in the collection by a field or using a callback. * * @param null|(callable(TValue, TKey): array-key)|string $countBy * @return static */ public function countBy($countBy = null) { return new static($this->lazy()->countBy($countBy)->all()); } /** * Sort the collection using multiple comparisons. * * @return static */ protected function sortByMany(array $comparisons = []) { $items = $this->items; usort($items, 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'; $result = 0; 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); } /** * 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; } /** * Results array of items from Collection or Arrayable. * @param null|Arrayable|iterable|Jsonable|JsonSerializable|static $items * @return array */ protected function getArrayableItems($items): array { return match (true) { is_array($items) => $items, $items instanceof self => $items->all(), $items instanceof Arrayable => $items->toArray(), $items instanceof Jsonable => json_decode($items->__toString(), true), $items instanceof JsonSerializable => $items->jsonSerialize(), $items instanceof Traversable => iterator_to_array($items), default => (array) $items, }; } } src/Enumerable.php000064400000101455151360551360010136 0ustar00 * @extends IteratorAggregate */ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable { /** * Convert the collection to its string representation. */ public function __toString(): string; /** * Dynamically access collection proxies. * * @param string $key * @return mixed * * @throws Exception */ public function __get($key); /** * Create a new collection instance if the value isn't one already. * * @template TMakeKey of array-key * @template TMakeValue * * @param null|Arrayable|iterable $items * @return static */ public static function make(mixed $items = []): static; /** * Create a new instance by invoking the callback a given amount of times. */ public static function times(int $number, ?callable $callback = null): static; /** * Create a collection with the given range. */ public static function range(float|int|string $from, float|int|string $to): static; /** * Wrap the given value in a collection if applicable. * * @template TWrapValue * * @param iterable|TWrapValue $value * @return static */ public static function wrap(mixed $value): static; /** * 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(mixed $value): array; /** * Create a new instance with no items. */ public static function empty(): static; /** * Get all items in the enumerable. */ public function all(): array; /** * Alias for the "avg" method. * * @param null|(callable(TValue): float|int)|string $callback */ public function average(mixed $callback = null): null|float|int; /** * Get the median of a given key. * * @param null|array|string $key * @return null|float|int */ public function median($key = null); /** * Get the mode of a given key. * * @param null|array|string $key * @return null|array */ 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)|string|TValue $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 array-key|(callable(TValue): bool)|TValue $key * @param null|TValue $value * @return bool */ public function containsStrict($key, $value = null); /** * Get the average value of a given key. * * @param null|(callable(TValue): float|int)|string $callback */ public function avg(mixed $callback = null): null|float|int; /** * Determine if an item exists in the enumerable. * * @param (callable(TValue, TKey): bool)|string|TValue $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 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 Arrayable|iterable $items */ public function diff($items): static; /** * Get the items that are not present in the given items, using the callback. * * @param 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 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 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 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 Arrayable|iterable $items * @param callable(TKey, TKey): int $callback * @return static */ public function diffKeysUsing($items, callable $callback); /** * Retrieve duplicate items. * * @param null|(callable(TValue): bool)|string $callback * @param bool $strict * @return static */ public function duplicates($callback = null, $strict = false); /** * Retrieve duplicate items using strict comparison. * * @param null|(callable(TValue): bool)|string $callback * @return static */ public function duplicatesStrict($callback = null); /** * Execute a callback over each item. * * @param callable(TValue, TKey): mixed $callback */ public function each(callable $callback): static; /** * Execute a callback over each nested chunk of items. */ public function eachSpread(callable $callback): static; /** * Determine if all items pass the given truth test. * * @param (callable(TValue, TKey): bool)|string|TValue $key */ public function every(mixed $key, mixed $operator = null, mixed $value = null): bool; /** * Get all items except for those with the specified keys. * * @param array|Enumerable $keys */ public function except($keys): static; /** * Run a filter over each of the items. * * @param null|(callable(TValue): bool) $callback * @return static */ public function filter(?callable $callback = null); /** * Apply the callback if the given "value" is (or resolves to) truthy. * * @template TWhenReturnType * * @param bool $value * @param null|(callable($this): TWhenReturnType) $callback * @param null|(callable($this): TWhenReturnType) $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 null|(callable($this): TWhenEmptyReturnType) $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 null|(callable($this): TWhenNotEmptyReturnType) $default * @return $this|TWhenNotEmptyReturnType */ public function whenNotEmpty(callable $callback, ?callable $default = null); /** * Apply the callback if the given "value" is (or resolves to) truthy. * * @template TUnlessReturnType * * @param bool $value * @param (callable($this): TUnlessReturnType) $callback * @param null|(callable($this): TUnlessReturnType) $default * @return $this|TUnlessReturnType */ public function unless($value, ?callable $callback = null, ?callable $default = null); /** * Apply the callback unless the collection is empty. * * @template TUnlessEmptyReturnType * * @param callable($this): TUnlessEmptyReturnType $callback * @param null|(callable($this): TUnlessEmptyReturnType) $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 null|(callable($this): TUnlessNotEmptyReturnType) $default * @return $this|TUnlessNotEmptyReturnType */ public function unlessNotEmpty(callable $callback, ?callable $default = null); /** * Filter items by the given key value pair. */ public function where(callable|string $key, mixed $operator = null, mixed $value = null): static; /** * Filter items where the value for the given key is null. * * @param null|string $key * @return static */ public function whereNull($key = null); /** * Filter items where the value for the given key is not null. * * @param null|string $key * @return static */ public function whereNotNull($key = null); /** * Filter items by the given key value pair using strict comparison. */ public function whereStrict(string $key, mixed $value): static; /** * Filter items by the given key value pair. * * @param Arrayable|iterable $values */ public function whereIn(string $key, mixed $values, bool $strict = false): static; /** * Filter items by the given key value pair using strict comparison. * * @param Arrayable|iterable $values */ public function whereInStrict(string $key, mixed $values): static; /** * Filter items such that the value of the given key is between the given values. * * @param string $key * @param 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 Arrayable|iterable $values * @return static */ public function whereNotBetween($key, $values); /** * Filter items by the given key value pair. * * @param Arrayable|iterable $values */ public function whereNotIn(string $key, mixed $values, bool $strict = false): static; /** * Filter items by the given key value pair using strict comparison. * * @param Arrayable|iterable $values */ public function whereNotInStrict(string $key, mixed $values): static; /** * Filter the items, removing any items that don't match the given type(s). * * @template TWhereInstanceOf * * @param array>|class-string $type * @return static */ public function whereInstanceOf(array|string $type): static; /** * Get the first item from the enumerable passing the given truth test. * * @template TFirstDefault * * @param null|(callable(TValue,TKey): bool) $callback * @param (Closure(): TFirstDefault)|TFirstDefault $default * @return TFirstDefault|TValue */ public function first(?callable $callback = null, $default = null); /** * Get the first item by the given key value pair. * * @param string $key * @return null|TValue */ public function firstWhere(callable|string $key, mixed $operator = null, mixed $value = null): mixed; /** * Get a flattened array of the items in the collection. * * @return static */ public function flatten(float|int $depth = INF); /** * Flip the values with their keys. * * @return static */ public function flip(): self|static; /** * Get an item from the collection by key. * * @template TGetDefault * * @param TKey $key * @param (Closure(): TGetDefault)|TGetDefault $default * @return TGetDefault|TValue */ public function get($key, $default = null); /** * Group an associative array by a field or using a callback. * * @param array|(callable(TValue, TKey): array-key)|string $groupBy * @return static> */ public function groupBy($groupBy, bool $preserveKeys = false); /** * Key an associative array by a field or using a callback. * * @param array|(callable(TValue, TKey): array-key)|string $keyBy * @return static */ public function keyBy($keyBy); /** * Determine if an item exists in the collection by key. * * @param array|TKey $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. */ public function implode(array|callable|string $value, ?string $glue = null): string; /** * Intersect the collection with the given items. * * @param Arrayable|iterable $items */ public function intersect(mixed $items): static; /** * Intersect the collection with the given items, using the callback. * * @param 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 Arrayable|iterable $items * @return static */ public function intersectAssoc($items); /** * Intersect the collection with the given items with additional index check, using the callback. * * @param 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 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. */ public function isNotEmpty(): bool; /** * Determine if the collection contains a single item. * * @return bool */ public function containsOneItem(); /** * 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 null|(callable(TValue, TKey): bool) $callback * @param (Closure(): TLastDefault)|TLastDefault $default * @return TLastDefault|TValue */ 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): self|static; /** * Run a map over each nested chunk of items. */ public function mapSpread(callable $callback): static; /** * 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): static; /** * 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): static; /** * 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): (array|Collection) $callback * @return static */ public function flatMap(callable $callback): static; /** * Map the values into a new class. * * @template TMapIntoValue * * @param class-string $class * @return static */ public function mapInto(mixed $class): self|static; /** * Merge the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function merge($items); /** * Recursively merge the collection with the given items. * * @template TMergeRecursiveValue * * @param 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 Arrayable|iterable $values * @return static */ public function combine($values); /** * Union the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function union($items); /** * Get the min value of a given key. * * @param null|(callable(TValue):mixed)|string $callback */ public function min(mixed $callback = null): mixed; /** * Get the max value of a given key. * * @param null|(callable(TValue):mixed)|string $callback */ public function max(mixed $callback = null): mixed; /** * Create a new collection consisting of every n-th element. */ public function nth(int $step, int $offset = 0): static; /** * Get the items with the specified keys. * * @param null|array|Enumerable|string $keys */ public function only($keys): static; /** * "Paginate" the collection by slicing it into a smaller collection. */ public function forPage(int $page, int $perPage): static; /** * Partition the collection into two arrays using the given callback or key. * * @param (callable(TValue, TKey): bool)|string|TValue $key * @return static, static> */ public function partition(mixed $key, mixed $operator = null, mixed $value = null): static; /** * 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. * * @return static|TValue * * @throws InvalidArgumentException */ public function random(?int $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 TReduceReturnType */ public function reduce(callable $callback, mixed $initial = null): mixed; /** * Reduce the collection to multiple aggregate values. * * @param mixed ...$initial * @return array * * @throws UnexpectedValueException */ public function reduceSpread(callable $callback, ...$initial); /** * Replace the collection items with the given items. * * @param Arrayable|iterable $items * @return static */ public function replace($items); /** * Recursively replace the collection items with the given items. * * @param 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 callable(TValue,TKey): bool|TValue $value * @return bool|TKey */ public function search($value, bool $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. * * @return static */ public function sliding(int $size = 2, int $step = 1): static; /** * Skip the first {$count} items. */ public function skip(int $count): static; /** * Skip items in the collection until the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function skipUntil($value); /** * Skip items in the collection while the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function skipWhile($value); /** * Get a slice of items from the enumerable. */ public function slice(int $offset, ?int $length = null): static; /** * Split a collection into a certain number of groups. * * @return static */ public function split(int $numberOfGroups): static; /** * 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 ItemNotFoundException * @throws 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 $key * @param mixed $operator * @param mixed $value * @return TValue * @throws ItemNotFoundException */ public function firstOrFail($key = null, $operator = null, $value = null); /** * Chunk the collection into chunks of the given size. * * @return static */ public function chunk(int $size): static; /** * Chunk the collection into chunks with a callback. * * @param callable(TValue, TKey, static): bool $callback * @return static> */ public function chunkWhile(callable $callback); /** * Split a collection into a certain number of groups, and fill the first groups completely. * * @return static */ public function splitIn(int $numberOfGroups); /** * Sort through each item with a callback. * * @param null|(callable(TValue, TValue): int) $callback */ public function sort(?callable $callback = null): static; /** * Sort items in descending order. */ public function sortDesc(int $options = SORT_REGULAR): static; /** * Sort the collection using the given callback. * * @param array|(callable(TValue, TKey): mixed)|string $callback */ public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): static; /** * Sort the collection in descending order using the given callback. * * @param array|(callable(TValue, TKey): mixed)|string $callback */ public function sortByDesc($callback, int $options = SORT_REGULAR): static; /** * Sort the collection keys. */ public function sortKeys(int $options = SORT_REGULAR, bool $descending = false): static; /** * Sort the collection keys in descending order. */ public function sortKeysDesc(int $options = SORT_REGULAR): static; /** * Sort the collection keys using a callback. * * @param callable(TKey, TKey): int $callback */ public function sortKeysUsing(callable $callback): static; /** * Get the sum of the given values. * * @param null|(callable(TValue): mixed)|string $callback * @return mixed */ public function sum($callback = null); /** * Take the first or last {$limit} items. */ public function take(int $limit): static; /** * Take items in the collection until the given condition is met. * * @param callable(TValue,TKey): bool|TValue $value * @return static */ public function takeUntil($value); /** * Take items in the collection while the given condition is met. * * @param callable(TValue,TKey): bool|TValue $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): static; /** * 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): mixed; /** * 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 array|string $value * @return static */ public function pluck(array|string $value, ?string $key = null): self|static; /** * Create a collection of all elements that do not pass a given truth test. * * @param bool|(callable(TValue, TKey): bool)|TValue $callback */ public function reject(mixed $callback = true): static; /** * Convert a flatten "dot" notation array into an expanded array. */ public function undot(): static; /** * Return only unique items from the collection array. * * @param null|(callable(TValue, TKey): mixed)|string $key */ public function unique(mixed $key = null, bool $strict = false): static; /** * Return only unique items from the collection array using strict comparison. * * @param null|(callable(TValue, TKey): mixed)|string $key */ public function uniqueStrict(mixed $key = null): static; /** * Reset the keys on the underlying array. * * @return static */ public function values(); /** * Pad collection to the specified length with a value. * * @template TPadValue * * @param TPadValue $value * @return static */ public function pad(int $size, $value): self|static; /** * Get the values iterator. * * @return Traversable */ public function getIterator(): Traversable; /** * Count the number of items in the collection. */ public function count(): int; /** * Count the number of items in the collection by a field or using a callback. * * @param null|(callable(TValue, TKey): array-key)|string $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 Arrayable|iterable ...$items * @return static> */ public function zip($items): self|static; /** * Collect the values into a collection. * * @return Collection */ public function collect(): Collection; /** * Get the collection of items as a plain array. * * @return array */ public function toArray(): array; /** * Convert the object into something JSON serializable. */ public function jsonSerialize(): mixed; /** * Get the collection of items as JSON. * * @return string */ public function toJson(int $options = 0); /** * Get a CachingIterator instance. * * @return CachingIterator */ public function getCachingIterator(int $flags = CachingIterator::CALL_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. */ public static function proxy(string $method): void; } src/Functions.php000064400000012554151360551360010030 0ustar00|iterable $value * @return Collection */ function collect($value = []): Collection { return new Collection($value); } /** * Fill in data where it's missing. * * @param mixed $target * @param array|string $key * @param mixed $value * @return mixed */ function data_fill(&$target, $key, $value) { return data_set($target, $key, $value, false); } /** * Get an item from an array or object using "dot" notation. * * @param mixed $target * @param null|array|int|string $key * @param mixed $default * @return mixed */ function data_get($target, $key, $default = null) { if (is_null($key)) { return $target; } $key = is_array($key) ? $key : explode('.', $key); foreach ($key as $i => $segment) { unset($key[$i]); if (is_null($segment)) { return $target; } if ($segment === '*') { if ($target instanceof Collection) { $target = $target->all(); } elseif (! is_iterable($target)) { return value($default); } $result = []; foreach ($target as $item) { $result[] = data_get($item, $key); } return in_array('*', $key) ? Arr::collapse($result) : $result; } if (Arr::accessible($target) && Arr::exists($target, $segment)) { $target = $target[$segment]; } elseif (is_object($target) && isset($target->{$segment})) { $target = $target->{$segment}; } else { return value($default); } } return $target; } /** * Set an item on an array or object using dot notation. * * @param mixed $target * @param array|string $key * @param mixed $value * @param bool $overwrite * @return mixed */ function data_set(&$target, $key, $value, $overwrite = true) { $segments = is_array($key) ? $key : explode('.', $key); if (($segment = array_shift($segments)) === '*') { if (! Arr::accessible($target)) { $target = []; } if ($segments) { foreach ($target as &$inner) { data_set($inner, $segments, $value, $overwrite); } } elseif ($overwrite) { foreach ($target as &$inner) { $inner = $value; } } } elseif (Arr::accessible($target)) { if ($segments) { if (! Arr::exists($target, $segment)) { $target[$segment] = []; } data_set($target[$segment], $segments, $value, $overwrite); } elseif ($overwrite || ! Arr::exists($target, $segment)) { $target[$segment] = $value; } } elseif (is_object($target)) { if ($segments) { if (! isset($target->{$segment})) { $target->{$segment} = []; } data_set($target->{$segment}, $segments, $value, $overwrite); } elseif ($overwrite || ! isset($target->{$segment})) { $target->{$segment} = $value; } } else { $target = []; if ($segments) { /* @phpstan-ignore-next-line */ data_set($target[$segment], $segments, $value, $overwrite); } elseif ($overwrite) { $target[$segment] = $value; } } return $target; } if (! function_exists('data_forget')) { /** * Remove / unset an item from an array or object using "dot" notation. * * @param mixed $target * @param null|array|int|string $key * @return mixed */ function data_forget(&$target, $key) { $segments = is_array($key) ? $key : explode('.', $key); if (($segment = array_shift($segments)) === '*' && Arr::accessible($target)) { if ($segments) { foreach ($target as &$inner) { data_forget($inner, $segments); } } } elseif (Arr::accessible($target)) { if ($segments && Arr::exists($target, $segment)) { data_forget($target[$segment], $segments); } else { Arr::forget($target, $segment); } } elseif (is_object($target)) { if ($segments && isset($target->{$segment})) { data_forget($target->{$segment}, $segments); } elseif (isset($target->{$segment})) { unset($target->{$segment}); } } return $target; } } /** * Get the first element of an array. Useful for method chaining. * * @param array $array * @return mixed */ function head($array) { return reset($array); } /** * Get the last element from an array. * * @param array $array * @return mixed */ function last($array) { return end($array); } /** * Return the default value of the given value. * * @param mixed ...$args * @return mixed */ function value(mixed $value, ...$args) { return $value instanceof Closure ? $value(...$args) : $value; } src/HigherOrderCollectionProxy.php000064400000002325151360551360013333 0ustar00collection->{$this->method}(function ($value) use ($key) { return is_array($value) ? $value[$key] : $value->{$key}; }); } /** * Proxy a method call onto the collection items. */ public function __call(string $method, array $parameters) { return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { return $value->{$method}(...$parameters); }); } } src/ItemNotFoundException.php000064400000000535151360551360012306 0ustar00 * / */ class LazyCollection implements Enumerable { use Macroable; /** * @use EnumeratesValues */ use EnumeratesValues; use Macroable; /** * The source from which to generate items. * * @var array|(Closure(): Generator)|static */ public $source; /** * Create a new lazy collection instance. * * @param null|array|Arrayable|(Closure(): Generator)|iterable|self $source */ 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 collection instance if the value isn't one already. * * @template TMakeKey of array-key * @template TMakeValue * * @param null|array|Arrayable|(Closure(): Generator)|iterable|self $items * @return static */ public static function make(mixed $items = []): static { return new static($items); } /** * Create a collection with the given range. * * @return static */ public static function range(float|int|string $from, float|int|string $to): static { return new static(function () use ($from, $to) { if ($from <= $to) { for (; $from <= $to; ++$from) { yield $from; } } else { for (; $from >= $to; --$from) { yield $from; } } }); } /** * Get all items in the enumerable. * * @return array */ public function all(): array { 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 null|array|string $key * @return null|float|int */ public function median($key = null) { return $this->collect()->median($key); } /** * Get the mode of a given key. * * @param null|array|string $key * @return null|array */ 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; } } } }); } /** * Determine if an item exists in the enumerable. * * @param (callable(TValue, TKey): bool)|string|TValue $key * @param mixed $operator * @param mixed $value */ public function contains($key, $operator = null, $value = null): bool { 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 array-key|(callable(TValue): bool)|TValue $key * @param null|TValue $value */ public function containsStrict($key, $value = null): bool { 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 */ public function doesntContain($key, $operator = null, $value = null): bool { return ! $this->contains(...func_get_args()); } /** * Cross join the given iterables, returning all possible permutations. * * @template TCrossJoinKey * @template TCrossJoinValue * * @param Arrayable|iterable ...$arrays * @return static> */ public function crossJoin(...$arrays) { return $this->passthru('crossJoin', func_get_args()); } /** * Count the number of items in the collection by a field or using a callback. * * @param null|(callable(TValue, TKey): array-key)|string $countBy * @return static */ 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 = $countBy($value, $key); if (empty($counts[$group])) { $counts[$group] = 0; } ++$counts[$group]; } yield from $counts; }); } /** * Get the items that are not present in the given items. * * @param Arrayable|iterable $items */ public function diff($items): static { return $this->passthru('diff', func_get_args()); } /** * Get the items that are not present in the given items, using the callback. * * @param Arrayable|iterable $items * @param callable(TValue, TValue): int $callback * @return static */ public function diffUsing($items, callable $callback) { return $this->passthru('diffUsing', func_get_args()); } /** * Get the items whose keys and values are not present in the given items. * * @param Arrayable|iterable $items * @return static */ public function diffAssoc($items) { return $this->passthru('diffAssoc', func_get_args()); } /** * Get the items whose keys and values are not present in the given items, using the callback. * * @param Arrayable|iterable $items * @param callable(TKey, TKey): int $callback * @return static */ public function diffAssocUsing($items, callable $callback) { return $this->passthru('diffAssocUsing', func_get_args()); } /** * Get the items whose keys are not present in the given items. * * @param Arrayable|iterable $items * @return static */ public function diffKeys($items) { return $this->passthru('diffKeys', func_get_args()); } /** * Get the items whose keys are not present in the given items, using the callback. * * @param Arrayable|iterable $items * @param callable(TKey, TKey): int $callback * @return static */ public function diffKeysUsing($items, callable $callback) { return $this->passthru('diffKeysUsing', func_get_args()); } /** * Retrieve duplicate items. * * @param null|(callable(TValue): bool)|string $callback * @param bool $strict * @return static */ public function duplicates($callback = null, $strict = false) { return $this->passthru('duplicates', func_get_args()); } /** * Retrieve duplicate items using strict comparison. * * @param null|(callable(TValue): bool)|string $callback * @return static */ public function duplicatesStrict($callback = null) { return $this->passthru('duplicatesStrict', func_get_args()); } /** * Get all items except for those with the specified keys. * * @param array|Enumerable $keys */ public function except($keys): static { return $this->passthru('except', func_get_args()); } /** * Run a filter over each of the items. * * @param null|(callable(TValue, TKey): bool) $callback */ public function filter(?callable $callback = null): static { 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 null|(callable(TValue,TKey): bool) $callback * @param (Closure(): TFirstDefault)|TFirstDefault $default * @return TFirstDefault|TValue */ public function first(?callable $callback = null, $default = null) { /** @var Iterator $iterator */ $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. * * @return static */ public function flatten(float|int $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(): self|static { return new static(function () { foreach ($this as $key => $value) { yield $value => $key; } }); } /** * Get an item by key. * * @template TGetDefault * * @param null|TKey $key * @param (Closure(): TGetDefault)|TGetDefault $default * @return TGetDefault|TValue */ public function get($key, $default = null) { if (is_null($key)) { return; } foreach ($this as $outerKey => $outerValue) { if ($outerKey == $key) { return $outerValue; } } return value($default); } /** * Group an associative array by a field or using a callback. * * @param array|(callable(TValue, TKey): array-key)|string $groupBy * @return static> */ public function groupBy($groupBy, bool $preserveKeys = false) { return $this->passthru('groupBy', func_get_args()); } /** * Key an associative array by a field or using a callback. * * @param array|(callable(TValue, TKey): array-key)|string $keyBy * @return static */ public function keyBy($keyBy) { return new static(function () use ($keyBy) { $keyBy = $this->valueRetriever($keyBy); foreach ($this as $key => $item) { $resolvedKey = $keyBy($item, $key); if (is_object($resolvedKey)) { $resolvedKey = (string) $resolvedKey; } yield $resolvedKey => $item; } }); } /** * Determine if an item exists in the collection by key. * * @param mixed $key */ public function has($key): bool { $keys = array_flip(is_array($key) ? $key : func_get_args()); $count = count($keys); foreach ($this as $key => $value) { if (array_key_exists($key, $keys) && --$count == 0) { return true; } } return false; } /** * Determine if any of the keys exist in the collection. * * @param mixed $key */ public function hasAny($key): bool { $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. */ public function implode(array|callable|string $value, ?string $glue = null): string { return $this->collect()->implode(...func_get_args()); } /** * Intersect the collection with the given items. * * @param Arrayable|iterable $items */ public function intersect(mixed $items): static { return $this->passthru('intersect', func_get_args()); } /** * Intersect the collection with the given items, using the callback. * * @param Arrayable|iterable $items * @param callable(TValue, TValue): int $callback * @return static */ public function intersectUsing($items, callable $callback) { return $this->passthru('intersectUsing', func_get_args()); } /** * Intersect the collection with the given items with additional index check. * * @param Arrayable|iterable $items * @return static */ public function intersectAssoc($items) { return $this->passthru('intersectAssoc', func_get_args()); } /** * Intersect the collection with the given items with additional index check, using the callback. * * @param Arrayable|iterable $items * @param callable(TValue, TValue): int $callback * @return static */ public function intersectAssocUsing($items, callable $callback) { return $this->passthru('intersectAssocUsing', func_get_args()); } /** * Intersect the collection with the given items by key. * * @param Arrayable|iterable $items * @return static */ public function intersectByKeys($items) { return $this->passthru('intersectByKeys', func_get_args()); } /** * Determine if the items are empty or not. */ public function isEmpty(): bool { return ! $this->getIterator()->valid(); } /** * Determine if the collection contains a single item. */ public function containsOneItem(): bool { return $this->take(2)->count() === 1; } /** * 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 null|(callable(TValue, TKey): bool) $callback * @param (Closure(): TLastDefault)|TLastDefault $default * @return TLastDefault|TValue */ 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 array|string $value * @return static */ public function pluck(array|string $value, ?string $key = null): static { return new static(function () use ($value, $key) { [$value, $key] = $this->explodePluckParameters($value, $key); foreach ($this as $item) { $itemValue = data_get($item, $value); if (is_null($key)) { yield $itemValue; } else { $itemKey = 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): self|static { return new static(function () use ($callback) { foreach ($this as $key => $value) { yield $key => $callback($value, $key); } }); } /** * 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): static { return $this->passthru('mapToDictionary', 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); } }); } /** * Merge the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function merge($items) { return $this->passthru('merge', func_get_args()); } /** * Recursively merge the collection with the given items. * * @template TMergeRecursiveValue * * @param Arrayable|iterable $items * @return static */ public function mergeRecursive($items) { return $this->passthru('mergeRecursive', func_get_args()); } /** * Create a collection by using this collection for keys and another for its values. * * @template TCombineValue * * @param array|(callable(): Generator)|IteratorAggregate $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); } }); } /** * Union the collection with the given items. * * @param Arrayable|iterable $items * @return static */ public function union($items) { return $this->passthru('union', func_get_args()); } /** * Create a new collection consisting of every n-th element. */ public function nth(int $step, int $offset = 0): static { 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 null|array|Enumerable|string $keys */ public function only($keys): static { 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 null|array|Enumerable|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. * * @return static|TValue * * @throws InvalidArgumentException */ public function random(?int $number = null) { $result = $this->collect()->random(...func_get_args()); return is_null($number) ? $result : new static($result); } /** * Replace the collection items with the given items. * * @param 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; } }); } /** * Recursively replace the collection items with the given items. * * @param Arrayable|iterable $items * @return static */ public function replaceRecursive($items) { return $this->passthru('replaceRecursive', func_get_args()); } /** * Reverse items order. * * @return static */ public function reverse() { return $this->passthru('reverse', func_get_args()); } /** * Search the collection for a given value and return the corresponding key if successful. * * @param (callable(TValue,TKey): bool)|TValue $value * @return false|TKey */ public function search($value, bool $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; } /** * Shuffle the items in the collection. * * @return Collection */ public function shuffle(?int $seed = null) { return $this->passthru('shuffle', []); } /** * Create chunks representing a "sliding window" view of the items in the collection. * * @return static */ public function sliding(int $size = 2, int $step = 1): static { 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. */ public function skip(int $count): static { 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 callable(TValue,TKey): bool|TValue $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 callable(TValue,TKey): bool|TValue $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(); } }); } /** * Get a slice of items from the enumerable. */ public function slice(int $offset, ?int $length = null): static { if ($offset < 0 || $length < 0) { return $this->passthru('slice', func_get_args()); } $instance = $this->skip($offset); return is_null($length) ? $instance : $instance->take($length); } /** * Split a collection into a certain number of groups. * * @return static */ public function split(int $numberOfGroups): static { return $this->passthru('split', 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 $key * @param mixed $operator * @param mixed $value * @return TValue * * @throws ItemNotFoundException * @throws 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(); } /** * 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 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. * * @return static */ public function chunk(int $size): static { if ($size <= 0) { return static::empty(); } return new static(function () use ($size) { $iterator = $this->getIterator(); while ($iterator->valid()) { $chunk = []; while (true) { $chunk[$iterator->key()] = $iterator->current(); 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. * * @return static */ public function splitIn(int $numberOfGroups) { return $this->chunk(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); } }); } /** * Sort through each item with a callback. * * @param null|(callable(TValue, TValue): int) $callback */ public function sort(?callable $callback = null): static { return $this->passthru('sort', func_get_args()); } /** * Sort items in descending order. */ public function sortDesc(int $options = SORT_REGULAR): static { return $this->passthru('sortDesc', func_get_args()); } /** * Sort the collection using the given callback. * * @param array|(callable(TValue, TKey): mixed)|string $callback * @param bool $descending */ public function sortBy($callback, int $options = SORT_REGULAR, $descending = false): static { return $this->passthru('sortBy', func_get_args()); } /** * Sort the collection in descending order using the given callback. * * @param array|(callable(TValue, TKey): mixed)|string $callback */ public function sortByDesc($callback, int $options = SORT_REGULAR): static { return $this->passthru('sortByDesc', func_get_args()); } /** * Sort the collection keys. */ public function sortKeys(int $options = SORT_REGULAR, bool $descending = false): static { return $this->passthru('sortKeys', func_get_args()); } /** * Sort the collection keys in descending order. */ public function sortKeysDesc(int $options = SORT_REGULAR): static { return $this->passthru('sortKeysDesc', func_get_args()); } /** * Sort the collection keys using a callback. * * @param callable(TKey, TKey): int $callback */ public function sortKeysUsing(callable $callback): static { return $this->passthru('sortKeysUsing', func_get_args()); } /** * Take the first or last {$limit} items. */ public function take(int $limit): static { 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 callable(TValue,TKey): bool|TValue $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. * * @return static */ public function takeUntilTimeout(DateTimeInterface $timeout) { $timeout = $timeout->getTimestamp(); return new static(function () use ($timeout) { if ($this->now() >= $timeout) { return; } foreach ($this as $key => $value) { yield $key => $value; if ($this->now() >= $timeout) { break; } } }); } /** * Take items in the collection while the given condition is met. * * @param callable(TValue,TKey): bool|TValue $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. * * @return static */ public function dot() { return $this->passthru('dot', []); } /** * Convert a flatten "dot" notation array into an expanded array. */ public function undot(): static { return $this->passthru('undot', []); } /** * Return only unique items from the collection array. * * @param null|(callable(TValue, TKey): mixed)|string $key */ public function unique(mixed $key = null, bool $strict = false): static { $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; } }); } /** * 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 Arrayable|iterable ...$items * @return static> */ public function zip($items): self|static { $iterables = func_get_args(); return new static(function () use ($iterables) { $iterators = Collection::make($iterables)->map(function ($iterable) { return $this->makeIterator($iterable); })->prepend($this->getIterator()); while ($iterators->contains->valid()) { yield new static($iterators->map->current()); $iterators->each->next(); } }); } /** * Pad collection to the specified length with a value. * * @template TPadValue * * @param TPadValue $value * @return static */ public function pad(int $size, $value): self|static { if ($size < 0) { return $this->passthru('pad', 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 Iterator */ public function getIterator(): Traversable { return $this->makeIterator($this->source); } /** * Count the number of items in the collection. */ 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 array|(callable(): mixed|Generator)|IteratorAggregate $source * @return Iterator * @throws Exception */ 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 null|string|string[] $key * @return array{string[],null|string[]} */ protected function explodePluckParameters($value, $key) { $value = is_string($value) ? explode('.', $value) : $value; $key = is_null($key) || is_array($key) ? $key : explode('.', $key); return [$value, $key]; } /** * Pass this lazy collection through a method on the collection class. * * @param string $method * @param array $params */ protected function passthru($method, array $params): static { 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()->timestamp : 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. */ protected function usleep(int $microseconds) { if ($microseconds <= 0) { return; } usleep($microseconds); } } src/MultipleItemsFoundException.php000064400000001554151360551360013526 0ustar00count = $count; parent::__construct("{$count} items were found.", $code, $previous); } /** * Get the number of items found. */ public function getCount(): int { return $this->count; } } src/Traits/EnumeratesValues.php000064400000075567151360551360012633 0ustar00 */ protected static $proxies = [ 'average', 'avg', 'contains', 'doesntContain', 'each', 'every', 'filter', 'first', 'flatMap', 'groupBy', 'keyBy', 'map', 'max', 'min', 'partition', 'percentage', 'reject', 'skipUntil', 'skipWhile', 'some', 'sortBy', 'sortByDesc', 'sum', 'takeUntil', 'takeWhile', 'unique', 'unless', 'until', 'when', ]; /** * Convert the collection to its string representation. */ public function __toString(): string { return $this->toJson(); } /** * 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); } /** * Create a new collection instance if the value isn't one already. * * @template TMakeKey of array-key * @template TMakeValue * * @param null|Arrayable|iterable $items * @return static */ public static function make(mixed $items = []): static { return new static($items); } /** * Wrap the given value in a collection if applicable. * * @template TWrapValue * * @param iterable|TWrapValue $value * @return static */ public static function wrap(mixed $value): static { return $value instanceof Enumerable ? new static($value) : new static(Arr::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(mixed $value): array { return $value instanceof Enumerable ? $value->all() : $value; } /** * Create a new instance with no items. */ public static function empty(): static { return new static([]); } /** * Create a new collection by invoking the callback a given amount of times. * * @param null|(callable(int): TTimesValue) $callback * @return static */ public static function times(int $number, ?callable $callback = null): static { if ($number < 1) { return new static(); } return static::range(1, $number) ->unless($callback == null) ->map($callback); } /** * Get the average value of a given key. * * @param null|(callable(TValue): float|int)|string $callback */ public function avg(mixed $callback = null): null|float|int { $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 null|(callable(TValue): float|int)|string $callback */ public function average(mixed $callback = null): null|float|int { return $this->avg($callback); } /** * Alias for the "contains" method. * * @param (callable(TValue, TKey): bool)|string|TValue $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) { $this->dump(...$args); exit(1); } /** * Dump the items. * * @param mixed ...$args * @return $this */ public function dump(...$args) { if (! function_exists('dump')) { throw new RuntimeException('symfony/var-dumper package required, please require the package via "composer require symfony/var-dumper"'); } dump($this->all(), ...$args); return $this; } /** * Execute a callback over each item. * * @param callable(TValue, TKey): mixed $callback */ public function each(callable $callback): static { 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 */ public function eachSpread(callable $callback): static { 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)|string|TValue $key */ public function every(mixed $key, mixed $operator = null, mixed $value = null): bool { 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. * * @return null|TValue */ public function firstWhere(callable|string $key, mixed $operator = null, mixed $value = null): mixed { return $this->first($this->operatorForWhere(...func_get_args())); } /** * Get a single key's value from the first matching item in the collection. * * @template TValueDefault * * @param string $key * @param (Closure(): TValueDefault)|TValueDefault $default * @return TValue|TValueDefault */ public function value($key, $default = null) { if ($value = $this->firstWhere($key)) { return data_get($value, $key, $default); } return value($default); } /** * Ensure that every item in the collection is of the expected type. * * @template TEnsureOfType * * @param array>|class-string $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. */ public function isNotEmpty(): bool { 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): static { 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): static { $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): (array|Collection) $callback * @return static */ public function flatMap(callable $callback): static { return $this->map($callback)->collapse(); } /** * Map the values into a new class. * * @template TMapIntoValue * @param class-string $class * @return static */ public function mapInto(mixed $class): self|static { 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. * * @param null|(callable(TValue):mixed)|string $callback */ public function min(mixed $callback = null): mixed { $callback = $this->valueRetriever($callback); return $this->map(fn ($value) => $callback($value)) ->filter(fn ($value) => ! is_null($value)) ->reduce(fn ($result, $value) => is_null($result) || $value < $result ? $value : $result); } /** * Get the max value of a given key. * * @param null|(callable(TValue):mixed)|string $callback */ public function max(mixed $callback = null): mixed { $callback = $this->valueRetriever($callback); return $this->filter(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. */ public function forPage(int $page, int $perPage): static { $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)|string|TValue $key * @param null|string|TValue $operator * @param null|TValue $value * @return static, static> */ public function partition(mixed $key, mixed $operator = null, mixed $value = null): static { $passed = []; $failed = []; $callback = func_num_args() === 1 ? $this->valueRetriever($key) : $this->operatorForWhere(...func_get_args()); foreach ($this as $key => $item) { if ($callback($item, $key)) { $passed[$key] = $item; } else { $failed[$key] = $item; } } return new static([new static($passed), new static($failed)]); } /** * Calculate the percentage of items that pass a given truth test. * * @param (callable(TValue, TKey): bool) $callback * @return null|float */ 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. * * @param null|(callable(TValue): mixed)|string $callback * @return mixed */ public function sum($callback = null) { $callback = is_null($callback) ? $this->identity() : $this->valueRetriever($callback); return $this->reduce(fn ($result, $item) => $result + $callback($item), 0); } /** * Apply the callback if the collection is empty. * * @template TWhenEmptyReturnType * * @param (callable($this): TWhenEmptyReturnType) $callback * @param null|(callable($this): TWhenEmptyReturnType) $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 null|(callable($this): TWhenNotEmptyReturnType) $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 null|(callable($this): TUnlessEmptyReturnType) $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 null|(callable($this): TUnlessNotEmptyReturnType) $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. */ public function where(null|callable|string $key, mixed $operator = null, mixed $value = null): static { return $this->filter($this->operatorForWhere(...func_get_args())); } /** * Filter items where the value for the given key is null. * * @param null|string $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 null|string $key * @return static */ public function whereNotNull($key = null) { return $this->where($key, '!==', null); } /** * Filter items by the given key value pair using strict comparison. */ public function whereStrict(?string $key, mixed $value): static { return $this->where($key, '===', $value); } /** * Filter items by the given key value pair. * * @param Arrayable|iterable $values */ public function whereIn(string $key, mixed $values, bool $strict = false): static { $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 Arrayable|iterable $values */ public function whereInStrict(string $key, mixed $values): static { 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 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 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 Arrayable|iterable $values */ public function whereNotIn(string $key, mixed $values, bool $strict = false): static { $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 Arrayable|iterable $values */ public function whereNotInStrict(string $key, mixed $values): static { return $this->whereNotIn($key, $values, true); } /** * Filter the items, removing any items that don't match the given type(s). * * @template TWhereInstanceOf * * @param array>|class-string $type * @return static */ public function whereInstanceOf(array|string $type): static { return $this->filter(function ($value) use ($type) { if (is_array($type)) { foreach ($type as $classType) { if ($value instanceof $classType) { return true; } } return false; } 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): mixed { 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 Collection::make($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 TReduceReturnType */ public function reduce(callable $callback, mixed $initial = null): mixed { $result = $initial; foreach ($this as $key => $value) { $result = $callback($result, $value, $key); } return $result; } /** * Reduce the collection to multiple aggregate values. * * @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 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 bool|(callable(TValue, TKey): bool)|TValue $callback */ public function reject(mixed $callback = true): static { $useAsCallable = $this->useAsCallable($callback); return $this->filter(function ($value, $key) use ($callback, $useAsCallable) { return $useAsCallable ? ! $callback($value, $key) : $value != $callback; }); } /** * Pass the collection to the given callback and then return it. * * @param callable($this): mixed $callback * @return $this */ public function tap(callable $callback): static { $callback($this); return $this; } /** * Return only unique items from the collection array. * * @param null|(callable(TValue, TKey): mixed)|string $key */ public function unique(mixed $key = null, bool $strict = false): static { $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 false; }); } /** * Return only unique items from the collection array using strict comparison. * * @param null|(callable(TValue, TKey): mixed)|string $key */ public function uniqueStrict(mixed $key = null): static { return $this->unique($key, true); } /** * Collect the values into a collection. * * @return Collection */ public function collect(): Collection { return new Collection($this->all()); } /** * Get the collection of items as a plain array. * * @return array */ public function toArray(): array { 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 { $result = []; foreach ($this->all() as $key => $value) { $result[$key] = match (true) { $value instanceof JsonSerializable => $value->jsonSerialize(), $value instanceof Jsonable => json_decode($value->__toString(), true), $value instanceof Arrayable => $value->toArray(), default => $value }; } return $result; } /** * Get the collection of items as JSON. * * @return string */ public function toJson(int $options = 0) { return json_encode($this->jsonSerialize(), $options); } /** * Get a CachingIterator instance. * * @return CachingIterator */ public function getCachingIterator(int $flags = CachingIterator::CALL_TOSTRING) { /* @phpstan-ignore-next-line */ return new CachingIterator($this->getIterator(), $flags); } /** * 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. */ public static function proxy(string $method): void { static::$proxies[] = $method; } /** * Results array of items from Collection or Arrayable. * @param mixed $items * @return array */ protected function getArrayableItems($items): array { return match (true) { is_array($items) => $items, $items instanceof Enumerable => $items->all(), $items instanceof Arrayable => $items->toArray(), $items instanceof Jsonable => json_decode($items->__toString(), true), $items instanceof JsonSerializable => $items->jsonSerialize(), $items instanceof Traversable => iterator_to_array($items), $items instanceof UnitEnum => [$items], default => (array) $items, }; } /** * Get an operator checker callback. * * @param callable|string $key * @param null|string $operator */ protected function operatorForWhere(mixed $key, mixed $operator = null, mixed $value = null): Closure { 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 = data_get($item, $key); $strings = array_filter([$retrieved, $value], function ($value) { return is_string($value) || (is_object($value) && method_exists($value, '__toString')); }); 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 */ protected function useAsCallable($value): bool { return ! is_string($value) && is_callable($value); } /** * Get a value retrieving callback. * * @param null|callable|string $value */ protected function valueRetriever($value): callable { 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. * * @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; } }