.gitattributes000064400000000054151360553250007443 0ustar00/tests export-ignore /.github export-ignore LICENSE000064400000002110151360553250005550 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.json000064400000002041151360553250007270 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/contract": "~3.1.0", "hyperf/macroable": "~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.0-dev" } } } src/Arr.php000064400000041472151360553250006605 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|string|int $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, int|float $depth = INF): array { $result = []; foreach ($array as $item) { $item = $item instanceof Collection ? $item->all() : $item; if (! is_array($item)) { $result[] = $item; } elseif ($depth === 1) { $result = array_merge($result, array_values($item)); } else { $result = array_merge($result, static::flatten($item, $depth - 1)); } } 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|string|int $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, int|string|null $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, int|string|null|array $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('.', $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, int|string|null|array $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 TKey * @template TValue * @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, int|string|array $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. * * @template TKey of array-key * @template TValue * * @param array $array * @param null|TKey $key * @param TValue $value * @return array */ public static function prepend(array $array, mixed $value, string|int|null $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): array { foreach ($array as &$value) { if (is_array($value)) { $value = static::sortRecursive($value); } } if (static::isAssoc($array)) { ksort($array); } else { sort($array); } 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; } /** * 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.php000064400000146235151360553250010157 0ustar00 * @implements Arrayable * @implements IteratorAggregate * * @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 ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable { use Macroable; /** * The items contained in the collection. * * @var array */ protected array $items = []; /** * The methods that can be proxied. * * @var string[] */ protected static array $proxies = [ 'average', 'avg', 'contains', 'each', 'every', 'filter', 'first', 'flatMap', 'groupBy', 'keyBy', 'map', 'max', 'min', 'partition', 'reject', 'sortBy', 'sortByDesc', 'sum', 'unique', ]; /** * Create a new collection. * @param null|iterable|Jsonable|JsonSerializable $items */ public function __construct($items = []) { $this->items = $this->getArrayableItems($items); } /** * Convert the collection to its string representation. */ public function __toString(): string { return $this->toJson(); } /** * Dynamically access collection proxies. * * @throws Exception */ public function __get(string $key) { if (! in_array($key, static::$proxies)) { throw new Exception("Property [{$key}] does not exist on this collection instance."); } return new HigherOrderCollectionProxy($this, $key); } /** * @param null|iterable|Jsonable|JsonSerializable $items * @return static */ public function fill($items = []) { $this->items = $this->getArrayableItems($items); return $this; } /** * Create a new collection instance if the value isn't one already. * * @template TMakeKey of array-key * @template TMakeValue * * @param null|Arrayable|iterable|Jsonable|JsonSerializable $items * @return static */ public static function make($items = []): self { return new static($items); } /** * Wrap the given value in a collection if applicable. * * @template TWrapKey of array-key * @template TWrapValue * * @param iterable $value * @return static */ public static function wrap($value): self { return $value instanceof self ? 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($value): array { return $value instanceof self ? $value->all() : $value; } /** * Create a new collection by invoking the callback a given amount of times. * * @template TTimesValue * * @param (callable(int): TTimesValue)|null $callback * @return static */ public static function times(int $number, callable $callback = null): self { if ($number < 1) { return new static(); } if (is_null($callback)) { return new static(range(1, $number)); } return (new static(range(1, $number)))->map($callback); } /** * Get all of the items in the collection. * * @return array */ public function all(): array { return $this->items; } /** * Get the average value of a given key. * * @param (callable(TValue): float|int)|string|null $callback */ public function avg($callback = null) { $callback = $this->valueRetriever($callback); $items = $this->map(function ($value) use ($callback) { return $callback($value); })->filter(function ($value) { return ! is_null($value); }); if ($count = $items->count()) { return $items->sum() / $count; } return null; } /** * Alias for the "avg" method. * * @param (callable(TValue): float|int)|string|null $callback * @return null|float|int */ public function average($callback = null) { return $this->avg($callback); } /** * Get the median of a given key. * * @param null|array|string $key */ 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; } $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)|TValue|string $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 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): self { return new static(Arr::crossJoin($this->items, ...array_map([$this, 'getArrayableItems'], $lists))); } /** * Dump the collection and end the script. */ public function dd(...$args): void { call_user_func_array([$this, 'dump'], $args); exit(1); } /** * Dump the collection. */ public function dump(): self { $params = (new static(func_get_args())); $params->push($this)->each(function ($item) { if (! class_exists(VarDumper::class)) { throw new RuntimeException('symfony/var-dumper package required, please require the package via "composer require symfony/var-dumper"'); } VarDumper::dump($item); }); return $this; } /** * Get the items in the collection that are not present in the given items. * * @param Arrayable|iterable $items * @return static */ public function diff($items): self { 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): self { 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): self { 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): self { 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): self { 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): self { return new static(array_diff_ukey($this->items, $this->getArrayableItems($items), $callback)); } /** * Execute a callback over each item. * @param callable(TValue,TKey): mixed $callback */ public function each(callable $callback): self { foreach ($this->items as $key => $item) { if ($callback($item, $key) === false) { break; } } return $this; } /** * Execute a callback over each nested chunk of items. * @param callable(...mixed): mixed $callback * @return static */ public function eachSpread(callable $callback): self { return $this->each(function ($chunk, $key) use ($callback) { $chunk[] = $key; return $callback(...$chunk); }); } /** * Determine if all items in the collection pass the given test. * * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value */ public function every($key, $operator = null, $value = null): bool { if (func_num_args() === 1) { $callback = $this->valueRetriever($key); foreach ($this->items as $k => $v) { if (! $callback($v, $k)) { return false; } } return true; } return $this->every($this->operatorForWhere(...func_get_args())); } /** * Get all items except for those with the specified keys. * * @param null|array|static $keys * @return static */ public function except($keys): self { 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 (callable(TValue, TKey): bool)|null $callback * @return static */ public function filter(callable $callback = null): self { if ($callback) { return new static(Arr::where($this->items, $callback)); } return new static(array_filter($this->items)); } /** * Apply the callback if the value is truthy. * * @param callable($this, $value): $this $callback * @param callable($this, $value): $this $default * @return $this */ public function when(bool $value, callable $callback, callable $default = null): self { if ($value) { return $callback($this, $value); } if ($default) { return $default($this, $value); } return $this; } /** * Apply the callback if the value is falsy. * * @param callable($this): $this $callback * @param callable($this): null|$this $default * @return $this */ public function unless(bool $value, callable $callback, callable $default = null): self { return $this->when(! $value, $callback, $default); } /** * Filter items by the given key value pair. * * @param mixed $operator * @param mixed $value * @return static */ public function where(string $key, $operator = null, $value = null): self { return $this->filter($this->operatorForWhere(...func_get_args())); } /** * Filter items by the given key value pair using strict comparison. * * @param mixed $value * @return static */ public function whereStrict(string $key, $value): self { return $this->where($key, '===', $value); } /** * Filter items by the given key value pair. * * @param Arrayable|iterable $values * @return static */ public function whereIn(string $key, $values, bool $strict = false): self { $values = $this->getArrayableItems($values); return $this->filter(function ($item) use ($key, $values, $strict) { return in_array(data_get($item, $key), $values, $strict); }); } /** * Filter items by the given key value pair using strict comparison. * * @param Arrayable|iterable $values * @return static */ public function whereInStrict(string $key, $values): self { return $this->whereIn($key, $values, true); } /** * Filter items by the given key value pair. * * @param Arrayable|iterable $values * @return static */ public function whereNotIn(string $key, $values, bool $strict = false): self { $values = $this->getArrayableItems($values); return $this->reject(function ($item) use ($key, $values, $strict) { return in_array(data_get($item, $key), $values, $strict); }); } /** * Filter items by the given key value pair using strict comparison. * * @param Arrayable|iterable $values * @return static */ public function whereNotInStrict(string $key, $values): self { return $this->whereNotIn($key, $values, true); } /** * Filter the items, removing any items that don't match the given type. * * @param class-string $type * @return static */ public function whereInstanceOf(string $type): self { return $this->filter(function ($value) use ($type) { return $value instanceof $type; }); } /** * Get the first item from the collection. * * @template TFirstDefault * * @param (callable(TValue, TKey): bool)|null $callback * @param TFirstDefault|(\Closure(): 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. * * @param mixed $operator * @param mixed $value * @return null|TValue */ public function firstWhere(string $key, $operator, $value = null) { 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 { 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): self { 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 TGetDefault|(\Closure(): TGetDefault) $default * @return TGetDefault|TValue */ public function get($key, $default = null) { if ($this->offsetExists($key)) { return $this->items[$key]; } return value($default); } /** * Group an associative array by a field or using a callback. * @param mixed $groupBy */ public function groupBy($groupBy, bool $preserveKeys = false): self { 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 (callable(TValue, TKey): array-key)|array|string $keyBy * @return static> */ public function keyBy($keyBy): self { $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; } /** * Concatenate values of a given key as a string. */ public function implode(string $value, string $glue = null): string { $first = $this->first(); if (is_array($first) || is_object($first)) { 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($items): self { return new static(array_intersect($this->items, $this->getArrayableItems($items))); } /** * Intersect the collection with the given items by key. * @param Arrayable|iterable $items * @return static */ public function intersectByKeys($items): self { 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); } /** * Determine if the collection is not empty. */ public function isNotEmpty(): bool { return ! $this->isEmpty(); } /** * Get the keys of the collection items. * @return static */ public function keys(): self { return new static(array_keys($this->items)); } /** * Get the last item from the collection. * * @template TLastDefault * * @param (callable(TValue, TKey): bool)|null $callback * @param TLastDefault|(\Closure(): TLastDefault) $default * @return 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($value, ?string $key = null): self { 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 { $keys = array_keys($this->items); $items = array_map($callback, $this->items, $keys); return new static(array_combine($keys, $items)); } /** * Run a map over each nested chunk of items. * * @template TMapSpreadValue * * @param callable(mixed): TMapSpreadValue $callback * @return static */ public function mapSpread(callable $callback): self { return $this->map(function ($chunk, $key) use ($callback) { $chunk[] = $key; return $callback(...$chunk); }); } /** * 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): self { $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 a grouping map over the items. * The callback should return an associative array with a single key/value pair. */ public function mapToGroups(callable $callback): self { $groups = $this->mapToDictionary($callback); return $groups->map([$this, 'make']); } /** * 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): self { return new static(Arr::mapWithKeys($this->items, $callback)); } /** * Map a collection and flatten the result by a single level. * * @param callable(TValue, TKey): mixed $callback * @return static */ public function flatMap(callable $callback): self { return $this->map($callback)->collapse(); } /** * Map the values into a new class. * * @param class-string $class * @return static */ public function mapInto(string $class): self { return $this->map(function ($value, $key) use ($class) { return new $class($value, $key); }); } /** * Get the max value of a given key. * * @param (callable(TValue):mixed)|string|null $callback * @return TValue */ public function max($callback = null) { $callback = $this->valueRetriever($callback); return $this->filter(function ($value) { return ! is_null($value); })->reduce(function ($result, $item) use ($callback) { $value = $callback($item); return is_null($result) || $value > $result ? $value : $result; }); } /** * Merge the collection with the given items. * @param Arrayable|iterable $items * @return static */ public function merge($items): self { return new static(array_merge($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): self { 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): self { return new static($this->items + $this->getArrayableItems($items)); } /** * Get the min value of a given key. * * @param (callable(TValue):mixed)|string|null $callback * @return TValue */ public function min($callback = null) { $callback = $this->valueRetriever($callback); return $this->map(function ($value) use ($callback) { return $callback($value); })->filter(function ($value) { return ! is_null($value); })->reduce(function ($result, $value) { return is_null($result) || $value < $result ? $value : $result; }); } /** * Create a new collection consisting of every n-th element. * * @return static */ public function nth(int $step, int $offset = 0): self { $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): self { 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)); } /** * "Paginate" the collection by slicing it into a smaller collection. */ public function forPage(int $page, int $perPage): self { $offset = max(0, ($page - 1) * $perPage); return $this->slice($offset, $perPage); } /** * Partition the collection into two arrays using the given callback or key. * * @param callable(TValue, TKey) bool)|TValue|string $key * @param null|string|TValue $operator * @param null|TValue $value * @return static> */ public function partition($key, $operator = null, $value = null): self { $partitions = [new static(), new static()]; $callback = func_num_args() === 1 ? $this->valueRetriever($key) : $this->operatorForWhere(...func_get_args()); foreach ($this->items as $key => $item) { $partitions[(int) ! $callback($item, $key)][$key] = $item; } return new static($partitions); } /** * Pass the collection to the given callback and return the result. * * @template TPipeReturnType * * @param callable($this): TPipeReturnType $callback * @return TPipeReturnType */ public function pipe(callable $callback) { return $callback($this); } /** * 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): self { $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): self { $this->offsetSet(null, $value); return $this; } /** * Push all of the given items onto the collection. * * @param iterable $source * @return static */ public function concat($source): self { $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 TPullDefault|(\Closure(): 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): self { $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)); } /** * Reduce the collection to a single value. * * @template TReduceInitial * @template TReduceReturnType * * @param callable(TReduceInitial|TReduceReturnType, TValue): TReduceReturnType $callback * @param TReduceInitial $initial * @return TReduceInitial|TReduceReturnType */ public function reduce(callable $callback, $initial = null) { return array_reduce($this->items, $callback, $initial); } /** * Create a collection of all elements that do not pass a given truth test. * * @param callable(TValue, TKey): bool|bool $callback * @return static */ public function reject($callback): self { if ($this->useAsCallable($callback)) { return $this->filter(function ($value, $key) use ($callback) { return ! $callback($value, $key); }); } return $this->filter(function ($item) use ($callback) { return $item != $callback; }); } /** * Reverse items order. * * @return static */ public function reverse(): self { return new static(array_reverse($this->items, true)); } /** * Search the collection for a given value and return the corresponding key if successful. * * @param TValue|(callable(TValue,TKey): bool) $value * @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): self { return new static(Arr::shuffle($this->items, $seed)); } /** * Slice the underlying collection array. * * @return static */ public function slice(int $offset, int $length = null): self { return new static(array_slice($this->items, $offset, $length, true)); } /** * Split a collection into a certain number of groups. * * @return static> */ public function split(int $numberOfGroups): self { 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): self { 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): self { $items = $this->items; $callback ? uasort($items, $callback) : asort($items); return new static($items); } /** * Sort the collection using the given callback. * * @param (callable(TValue, TKey): mixed)|string|array $callback * @return static */ public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): self { 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): self { return $this->sortBy($callback, $options, true); } /** * Sort the collection keys. * * @return static */ public function sortKeys(int $options = SORT_REGULAR, bool $descending = false): self { $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): self { return $this->sortKeys($options, true); } /** * Splice a portion of the underlying collection array. * * @param array $replacement * @return static */ public function splice(int $offset, int $length = null, $replacement = []): self { if (func_num_args() === 1) { return new static(array_splice($this->items, $offset)); } return new static(array_splice($this->items, $offset, $length, $replacement)); } /** * Get the sum of the given values. * * @param (callable(TValue): mixed)|string|null $callback * @return mixed */ public function sum($callback = null) { if (is_null($callback)) { return array_sum($this->items); } $callback = $this->valueRetriever($callback); return $this->reduce(function ($result, $item) use ($callback) { return $result + $callback($item); }, 0); } /** * Take the first or last {$limit} items. * * @return static */ public function take(int $limit): self { if ($limit < 0) { return $this->slice($limit, abs($limit)); } return $this->slice(0, $limit); } /** * Pass the collection to the given callback and then return it. * * @param callable(static): mixed $callback * @return $this */ public function tap(callable $callback): self { $callback(new static($this->items)); return $this; } /** * Transform each item in the collection using a callback. * * @param callable(TValue, TKey): TValue $callback * @return $this */ public function transform(callable $callback): self { $this->items = $this->map($callback)->all(); return $this; } /** * Return only unique items from the collection array. * * @param (callable(TValue, TKey): mixed)|string|null $key * @return static */ public function unique($key = null, bool $strict = false): self { $callback = $this->valueRetriever($key); $exists = []; return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) { if (in_array($id = $callback($item, $key), $exists, $strict)) { return true; } $exists[] = $id; }); } /** * Return only unique items from the collection array using strict comparison. * * @param (callable(TValue, TKey): mixed)|string|null $key * @return static */ public function uniqueStrict($key = null): self { return $this->unique($key, true); } /** * Reset the keys on the underlying array. * * @return static */ public function values(): self { 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 { $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 { return new static(array_pad($this->items, $size, $value)); } /** * Get the collection of items as a plain array. * * @return array */ public function toArray(): array { return array_map(function ($value) { return $value instanceof Arrayable ? $value->toArray() : $value; }, $this->items); } /** * Convert the object into something JSON serializable. * * @return array */ public function jsonSerialize(): mixed { return array_map(function ($value) { if ($value instanceof JsonSerializable) { return $value->jsonSerialize(); } if ($value instanceof Jsonable) { return json_decode($value->__toString(), true); } if ($value instanceof Arrayable) { return $value->toArray(); } return $value; }, $this->items); } /** * Get the collection of items as JSON. */ public function toJson(int $options = 0): string { return json_encode($this->jsonSerialize(), $options); } /** * Get an iterator for the items. * * @return ArrayIterator */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->items); } /** * Get a CachingIterator instance. */ public function getCachingIterator(int $flags = CachingIterator::CALL_TOSTRING): CachingIterator { /* @phpstan-ignore-next-line */ return new CachingIterator($this->getIterator(), $flags); } /** * 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 array_key_exists($offset, $this->items); } /** * 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]); } /** * Add a method to the list of proxied methods. */ public static function proxy(string $method): void { static::$proxies[] = $method; } /** * 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 an operator checker callback. * @param mixed|string $operator * @param null|TValue $value */ protected function operatorForWhere(string $key, $operator = null, $value = null): Closure { 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; } }; } /** * 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 mixed $value */ protected function valueRetriever($value): callable { if ($this->useAsCallable($value)) { return $value; } return function ($item) use ($value) { return data_get($item, $value); }; } /** * Results array of items from Collection or Arrayable. * @param null|Arrayable|iterable|Jsonable|JsonSerializable|static $items * @return array */ protected function getArrayableItems($items): array { if (is_array($items)) { return $items; } if ($items instanceof self) { return $items->all(); } if ($items instanceof Arrayable) { return $items->toArray(); } if ($items instanceof Jsonable) { return json_decode($items->__toString(), true); } if ($items instanceof JsonSerializable) { return $items->jsonSerialize(); } if ($items instanceof Traversable) { return iterator_to_array($items); } return (array) $items; } } src/Functions.php000064400000012553151360553250010027 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.php000064400000002514151360553250013333 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); }); } }