items = $this->normalizeItems($items); } /** * Static constructor for fluent one-liners: Collection::make([...])->... */ public static function make($items = []): self { return new self($items); } private function normalizeItems($items): array { if (is_array($items)) { return $items; } if ($items instanceof self) { return $items->all(); } if ($items instanceof \Traversable) { return iterator_to_array($items); } if ($items === null) { return []; } return [$items]; } /* --------------------------------------------------------------------- * Output * ------------------------------------------------------------------- */ public function all(): array { return $this->items; } public function toArray(): array { return array_map(function ($value) { if ($value instanceof Model) { return $value->toArray(); } if ($value instanceof self) { return $value->toArray(); } if (is_object($value) && method_exists($value, 'toArray')) { return $value->toArray(); } return $value; }, $this->items); } public function toJson(int $options = 0): string { return wp_json_encode($this->toArray(), $options); } #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->toArray(); } /* --------------------------------------------------------------------- * Element access * ------------------------------------------------------------------- */ public function first(?callable $callback = null, $default = null) { if ($callback === null) { foreach ($this->items as $item) { return $item; } return $default instanceof \Closure ? $default() : $default; } foreach ($this->items as $key => $item) { if ($callback($item, $key)) { return $item; } } return $default instanceof \Closure ? $default() : $default; } public function last(?callable $callback = null, $default = null) { if ($callback === null) { return empty($this->items) ? ($default instanceof \Closure ? $default() : $default) : $this->items[array_key_last($this->items)]; } $result = $default; foreach ($this->items as $key => $item) { if ($callback($item, $key)) { $result = $item; } } return $result instanceof \Closure ? $result() : $result; } public function firstWhere($key, $operator = null, $value = null) { return $this->first($this->operatorForWhere($key, $operator, $value)); } public function get($key, $default = null) { if ($this->offsetExists($key)) { return $this->items[$key]; } return $default instanceof \Closure ? $default() : $default; } public function has($key): bool { $keys = is_array($key) ? $key : func_get_args(); foreach ($keys as $k) { if (!array_key_exists($k, $this->items)) { return false; } } return true; } /** * Retrieve the single matching item, or throw when zero / more than one match. */ public function sole($key = null, $operator = null, $value = null) { $filtered = is_callable($key) && $operator === null ? $this->filter($key) : ($key === null ? $this : $this->where($key, $operator, $value)); $count = $filtered->count(); if ($count === 0) { throw new \RuntimeException('Collection::sole() found no matching items.'); } if ($count > 1) { throw new \RuntimeException('Collection::sole() found ' . esc_html((string) $count) . ' items.'); } return $filtered->first(); } /* --------------------------------------------------------------------- * Transformation (return new Collection) * ------------------------------------------------------------------- */ public function map(callable $callback): self { $keys = array_keys($this->items); $items = array_map($callback, $this->items, $keys); return new self(array_combine($keys, $items)); } /** * Map, expecting each callback to return a single [key => value] pair. */ public function mapWithKeys(callable $callback): self { $result = []; foreach ($this->items as $key => $value) { $assoc = $callback($value, $key); foreach ($assoc as $mapKey => $mapValue) { $result[$mapKey] = $mapValue; } } return new self($result); } /** * Map then flatten one level. */ public function flatMap(callable $callback): self { return $this->map($callback)->collapse(); } public function filter(?callable $callback = null): self { if ($callback === null) { return new self(array_filter($this->items)); } return new self(array_filter($this->items, $callback, ARRAY_FILTER_USE_BOTH)); } public function reject(callable $callback): self { return $this->filter(function ($value, $key) use ($callback) { return !$callback($value, $key); }); } public function each(callable $callback): self { foreach ($this->items as $key => $item) { if ($callback($item, $key) === false) { break; } } return $this; } public function reduce(callable $callback, $initial = null) { $accumulator = $initial; foreach ($this->items as $key => $item) { $accumulator = $callback($accumulator, $item, $key); } return $accumulator; } public function pluck($value, $key = null): self { $results = []; foreach ($this->items as $item) { $itemValue = $this->valueOf($item, $value); if ($key !== null) { $results[$this->valueOf($item, $key)] = $itemValue; } else { $results[] = $itemValue; } } return new self($results); } public function keyBy($keyBy): self { $results = []; foreach ($this->items as $item) { $keyValue = is_callable($keyBy) ? $keyBy($item) : $this->valueOf($item, $keyBy); if ($keyValue !== null) { $results[(string) $keyValue] = $item; } } return new self($results); } public function groupBy($groupBy): self { $results = []; foreach ($this->items as $key => $item) { $groupKey = is_callable($groupBy) ? $groupBy($item, $key) : $this->valueOf($item, $groupBy); $groupKey = is_bool($groupKey) ? (int) $groupKey : $groupKey; $results[$groupKey][] = $item; } return (new self($results))->map(function ($group) { return new self($group); }); } public function sortBy($callback, int $options = SORT_REGULAR, bool $descending = false): self { $results = []; $getter = is_callable($callback) ? $callback : function ($item) use ($callback) { return $this->valueOf($item, $callback); }; foreach ($this->items as $key => $value) { $results[$key] = $getter($value, $key); } $descending ? arsort($results, $options) : asort($results, $options); foreach (array_keys($results) as $key) { $results[$key] = $this->items[$key]; } return new self($results); } public function sortByDesc($callback, int $options = SORT_REGULAR): self { return $this->sortBy($callback, $options, true); } public function sort(?callable $callback = null): self { $items = $this->items; $callback ? uasort($items, $callback) : asort($items); return new self($items); } public function unique($key = null, bool $strict = false): self { if ($key === null) { return new self(array_unique($this->items, SORT_REGULAR)); } $seen = []; $getter = is_callable($key) ? $key : function ($item) use ($key) { return $this->valueOf($item, $key); }; return $this->filter(function ($item) use ($getter, &$seen, $strict) { $id = $getter($item); if (in_array($id, $seen, $strict)) { return false; } $seen[] = $id; return true; }); } public function where($key, $operator = null, $value = null): self { return $this->filter($this->operatorForWhere($key, $operator, $value)); } public function whereIn($key, array $values, bool $strict = false): self { return $this->filter(function ($item) use ($key, $values, $strict) { return in_array($this->valueOf($item, $key), $values, $strict); }); } public function whereNotIn($key, array $values, bool $strict = false): self { return $this->reject(function ($item) use ($key, $values, $strict) { return in_array($this->valueOf($item, $key), $values, $strict); }); } public function only($keys): self { $keys = is_array($keys) ? $keys : func_get_args(); return new self(array_intersect_key($this->items, array_flip($keys))); } public function except($keys): self { $keys = is_array($keys) ? $keys : func_get_args(); return new self(array_diff_key($this->items, array_flip($keys))); } public function diff($items): self { return new self(array_diff($this->items, $this->normalizeItems($items))); } public function intersect($items): self { return new self(array_intersect($this->items, $this->normalizeItems($items))); } public function merge($items): self { return new self(array_merge($this->items, $this->normalizeItems($items))); } public function concat($items): self { $result = $this->items; foreach ($this->normalizeItems($items) as $item) { $result[] = $item; } return new self($result); } public function collapse(): self { $results = []; foreach ($this->items as $values) { if ($values instanceof self) { $values = $values->all(); } if (is_array($values)) { $results = array_merge($results, $values); } } return new self($results); } public function flatten(int $depth = PHP_INT_MAX): self { $result = []; foreach ($this->items as $item) { $item = $item instanceof self ? $item->all() : $item; if (!is_array($item)) { $result[] = $item; } elseif ($depth === 1) { $result = array_merge($result, array_values($item)); } else { $result = array_merge($result, (new self($item))->flatten($depth - 1)->all()); } } return new self($result); } public function chunk(int $size): self { if ($size <= 0) { return new self(); } $chunks = []; foreach (array_chunk($this->items, $size, true) as $chunk) { $chunks[] = new self($chunk); } return new self($chunks); } public function slice(int $offset, ?int $length = null): self { return new self(array_slice($this->items, $offset, $length, true)); } public function take(int $limit): self { if ($limit < 0) { return $this->slice($limit, abs($limit)); } return $this->slice(0, $limit); } public function splice(int $offset, ?int $length = null, $replacement = []): self { $items = $this->items; if ($length === null) { $removed = array_splice($items, $offset); } else { $removed = array_splice($items, $offset, $length, $this->normalizeItems($replacement)); } $this->items = $items; return new self($removed); } public function reverse(): self { return new self(array_reverse($this->items, true)); } public function values(): self { return new self(array_values($this->items)); } public function keys(): self { return new self(array_keys($this->items)); } public function pad(int $size, $value): self { return new self(array_pad($this->items, $size, $value)); } /** * Split into two collections: [passing, failing]. */ public function partition(callable $callback): self { $passed = []; $failed = []; foreach ($this->items as $key => $item) { if ($callback($item, $key)) { $passed[$key] = $item; } else { $failed[$key] = $item; } } return new self([new self($passed), new self($failed)]); } /** * Zip the collection with one or more arrays/collections by index. */ public function zip(...$items): self { $arrays = array_map(function ($item) { return $this->normalizeItems($item); }, $items); $result = []; foreach (array_values($this->items) as $i => $value) { $row = [$value]; foreach ($arrays as $array) { $row[] = array_values($array)[$i] ?? null; } $result[] = new self($row); } return new self($result); } /* --------------------------------------------------------------------- * Aggregates * ------------------------------------------------------------------- */ public function sum($callback = null) { if ($callback === null) { return array_sum($this->items); } $getter = $this->valueRetriever($callback); return $this->reduce(function ($carry, $item) use ($getter) { return $carry + $getter($item); }, 0); } public function avg($callback = null) { $count = $this->count(); if ($count === 0) { return null; } return $this->sum($callback) / $count; } public function average($callback = null) { return $this->avg($callback); } public function min($callback = null) { $getter = $this->valueRetriever($callback); $values = array_map($getter, $this->items); $values = array_filter($values, function ($v) { return $v !== null; }); return empty($values) ? null : min($values); } public function max($callback = null) { $getter = $this->valueRetriever($callback); $values = array_map($getter, $this->items); $values = array_filter($values, function ($v) { return $v !== null; }); return empty($values) ? null : max($values); } public function median($callback = null) { $getter = $this->valueRetriever($callback); $values = array_values(array_filter(array_map($getter, $this->items), function ($v) { return $v !== null; })); $count = count($values); if ($count === 0) { return null; } sort($values); $middle = (int) ($count / 2); if ($count % 2) { return $values[$middle]; } return ($values[$middle - 1] + $values[$middle]) / 2; } /* --------------------------------------------------------------------- * Predicates * ------------------------------------------------------------------- */ public function contains($key, $operator = null, $value = null): bool { if (func_num_args() === 1) { if (is_callable($key)) { return $this->first($key) !== null || $this->some($key); } return in_array($key, $this->items, false); } return $this->some($this->operatorForWhere($key, $operator, $value)); } public function every(callable $callback): bool { foreach ($this->items as $key => $item) { if (!$callback($item, $key)) { return false; } } return true; } public function some(callable $callback): bool { foreach ($this->items as $key => $item) { if ($callback($item, $key)) { return true; } } return false; } public function isEmpty(): bool { return empty($this->items); } public function isNotEmpty(): bool { return !$this->isEmpty(); } /* --------------------------------------------------------------------- * Mutating helpers (operate in place, return $this for chaining) * ------------------------------------------------------------------- */ public function push(...$values): self { foreach ($values as $value) { $this->items[] = $value; } return $this; } public function prepend($value, $key = null): self { if ($key === null) { array_unshift($this->items, $value); } else { $this->items = [$key => $value] + $this->items; } return $this; } public function put($key, $value): self { $this->items[$key] = $value; return $this; } public function forget($keys): self { foreach ((is_array($keys) ? $keys : [$keys]) as $key) { unset($this->items[$key]); } return $this; } public function pop() { return array_pop($this->items); } public function shift() { return array_shift($this->items); } /* --------------------------------------------------------------------- * Flow control / utility * ------------------------------------------------------------------- */ public function pipe(callable $callback) { return $callback($this); } public function tap(callable $callback): self { $callback($this); return $this; } public function when($condition, callable $callback, ?callable $default = null) { $value = $condition instanceof \Closure ? $condition($this) : $condition; if ($value) { return $callback($this, $value) ?? $this; } if ($default !== null) { return $default($this, $value) ?? $this; } return $this; } public function unless($condition, callable $callback, ?callable $default = null) { $value = $condition instanceof \Closure ? $condition($this) : $condition; return $this->when(!$value, $callback, $default); } public function implode($value, ?string $glue = null): string { if ($glue === null) { return implode($value, $this->items); } return implode($glue, $this->pluck($value)->all()); } /* --------------------------------------------------------------------- * Interfaces * ------------------------------------------------------------------- */ #[\ReturnTypeWillChange] public function getIterator(): \Traversable { return new \ArrayIterator($this->items); } #[\ReturnTypeWillChange] public function offsetExists($offset): bool { return array_key_exists($offset, $this->items); } #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->items[$offset] ?? null; } #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { if (is_null($offset)) { $this->items[] = $value; } else { $this->items[$offset] = $value; } } #[\ReturnTypeWillChange] public function offsetUnset($offset): void { unset($this->items[$offset]); } #[\ReturnTypeWillChange] public function count(): int { return count($this->items); } /* --------------------------------------------------------------------- * Internals * ------------------------------------------------------------------- */ /** * Read a value from a Model, array, or object by key. */ private function valueOf($item, $key) { if ($key instanceof \Closure) { return $key($item); } if ($item instanceof Model) { return $item->$key; } if (is_array($item) || $item instanceof \ArrayAccess) { return $item[$key] ?? null; } if (is_object($item)) { return $item->$key ?? null; } return null; } /** * Turn null|string|callable into a value getter for aggregate methods. */ private function valueRetriever($callback): callable { if ($callback === null) { return function ($item) { return $item; }; } if (is_callable($callback)) { return $callback; } return function ($item) use ($callback) { return $this->valueOf($item, $callback); }; } /** * Build a predicate for where()/firstWhere()/contains() from (key, op, value). */ private function operatorForWhere($key, $operator = null, $value = null): callable { if ($key instanceof \Closure) { return $key; } // where('key') → truthy check on that key. if (func_num_args() === 1) { return function ($item) use ($key) { return (bool) $this->valueOf($item, $key); }; } // where('key', $value) → equality shortcut. if ($value === null && !in_array($operator, ['=', '==', '===', '!=', '!==', '<>', '<', '<=', '>', '>='], true)) { $value = $operator; $operator = '='; } return function ($item) use ($key, $operator, $value) { $retrieved = $this->valueOf($item, $key); switch ($operator) { 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 '==': case '=': default: return $retrieved == $value; } }; } }