Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/*** This file is part of the ramsey/collection library** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.** @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>* @license http://opensource.org/licenses/MIT MIT*/declare(strict_types=1);namespace Ramsey\Collection;use Closure;use Ramsey\Collection\Exception\CollectionMismatchException;use Ramsey\Collection\Exception\InvalidArgumentException;use Ramsey\Collection\Exception\InvalidPropertyOrMethod;use Ramsey\Collection\Exception\NoSuchElementException;use Ramsey\Collection\Exception\UnsupportedOperationException;use Ramsey\Collection\Tool\TypeTrait;use Ramsey\Collection\Tool\ValueExtractorTrait;use Ramsey\Collection\Tool\ValueToStringTrait;use function array_filter;use function array_key_first;use function array_key_last;use function array_map;use function array_merge;use function array_reduce;use function array_search;use function array_udiff;use function array_uintersect;use function in_array;use function is_int;use function is_object;use function spl_object_id;use function sprintf;use function usort;/*** This class provides a basic implementation of `CollectionInterface`, to* minimize the effort required to implement this interface** @template T* @extends AbstractArray<T>* @implements CollectionInterface<T>*/abstract class AbstractCollection extends AbstractArray implements CollectionInterface{use TypeTrait;use ValueToStringTrait;use ValueExtractorTrait;/*** @throws InvalidArgumentException if $element is of the wrong type.*/public function add(mixed $element): bool{$this[] = $element;return true;}public function contains(mixed $element, bool $strict = true): bool{return in_array($element, $this->data, $strict);}/*** @throws InvalidArgumentException if $element is of the wrong type.*/public function offsetSet(mixed $offset, mixed $value): void{if ($this->checkType($this->getType(), $value) === false) {throw new InvalidArgumentException('Value must be of type ' . $this->getType() . '; value is '. $this->toolValueToString($value),);}if ($offset === null) {$this->data[] = $value;} else {$this->data[$offset] = $value;}}public function remove(mixed $element): bool{if (($position = array_search($element, $this->data, true)) !== false) {unset($this[$position]);return true;}return false;}/*** @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist* on the elements in this collection.* @throws UnsupportedOperationException if unable to call column() on this* collection.** @inheritDoc*/public function column(string $propertyOrMethod): array{$temp = [];foreach ($this->data as $item) {/** @psalm-suppress MixedAssignment */$temp[] = $this->extractValue($item, $propertyOrMethod);}return $temp;}/*** @return T** @throws NoSuchElementException if this collection is empty.*/public function first(): mixed{$firstIndex = array_key_first($this->data);if ($firstIndex === null) {throw new NoSuchElementException('Can\'t determine first item. Collection is empty');}return $this->data[$firstIndex];}/*** @return T** @throws NoSuchElementException if this collection is empty.*/public function last(): mixed{$lastIndex = array_key_last($this->data);if ($lastIndex === null) {throw new NoSuchElementException('Can\'t determine last item. Collection is empty');}return $this->data[$lastIndex];}/*** @return CollectionInterface<T>** @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist* on the elements in this collection.* @throws UnsupportedOperationException if unable to call sort() on this* collection.*/public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): CollectionInterface{$collection = clone $this;usort($collection->data,/*** @param T $a* @param T $b*/function (mixed $a, mixed $b) use ($propertyOrMethod, $order): int {/** @var mixed $aValue */$aValue = $this->extractValue($a, $propertyOrMethod);/** @var mixed $bValue */$bValue = $this->extractValue($b, $propertyOrMethod);return ($aValue <=> $bValue) * ($order === Sort::Descending ? -1 : 1);},);return $collection;}/*** @param callable(T): bool $callback A callable to use for filtering elements.** @return CollectionInterface<T>*/public function filter(callable $callback): CollectionInterface{$collection = clone $this;$collection->data = array_merge([], array_filter($collection->data, $callback));return $collection;}/*** @return CollectionInterface<T>** @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist* on the elements in this collection.* @throws UnsupportedOperationException if unable to call where() on this* collection.*/public function where(?string $propertyOrMethod, mixed $value): CollectionInterface{return $this->filter(/*** @param T $item*/function (mixed $item) use ($propertyOrMethod, $value): bool {/** @var mixed $accessorValue */$accessorValue = $this->extractValue($item, $propertyOrMethod);return $accessorValue === $value;},);}/*** @param callable(T): TCallbackReturn $callback A callable to apply to each* item of the collection.** @return CollectionInterface<TCallbackReturn>** @template TCallbackReturn*/public function map(callable $callback): CollectionInterface{/** @var Collection<TCallbackReturn> */return new Collection('mixed', array_map($callback, $this->data));}/*** @param callable(TCarry, T): TCarry $callback A callable to apply to each* item of the collection to reduce it to a single value.* @param TCarry $initial This is the initial value provided to the callback.** @return TCarry** @template TCarry*/public function reduce(callable $callback, mixed $initial): mixed{/** @var TCarry */return array_reduce($this->data, $callback, $initial);}/*** @param CollectionInterface<T> $other The collection to check for divergent* items.** @return CollectionInterface<T>** @throws CollectionMismatchException if the compared collections are of* differing types.*/public function diff(CollectionInterface $other): CollectionInterface{$this->compareCollectionTypes($other);$diffAtoB = array_udiff($this->data, $other->toArray(), $this->getComparator());$diffBtoA = array_udiff($other->toArray(), $this->data, $this->getComparator());/** @var array<array-key, T> $diff */$diff = array_merge($diffAtoB, $diffBtoA);$collection = clone $this;$collection->data = $diff;return $collection;}/*** @param CollectionInterface<T> $other The collection to check for* intersecting items.** @return CollectionInterface<T>** @throws CollectionMismatchException if the compared collections are of* differing types.*/public function intersect(CollectionInterface $other): CollectionInterface{$this->compareCollectionTypes($other);/** @var array<array-key, T> $intersect */$intersect = array_uintersect($this->data, $other->toArray(), $this->getComparator());$collection = clone $this;$collection->data = $intersect;return $collection;}/*** @param CollectionInterface<T> ...$collections The collections to merge.** @return CollectionInterface<T>** @throws CollectionMismatchException if unable to merge any of the given* collections or items within the given collections due to type* mismatch errors.*/public function merge(CollectionInterface ...$collections): CollectionInterface{$mergedCollection = clone $this;foreach ($collections as $index => $collection) {if (!$collection instanceof static) {throw new CollectionMismatchException(sprintf('Collection with index %d must be of type %s', $index, static::class),);}// When using generics (Collection.php, Set.php, etc),// we also need to make sure that the internal types match each otherif ($this->getUniformType($collection) !== $this->getUniformType($this)) {throw new CollectionMismatchException(sprintf('Collection items in collection with index %d must be of type %s',$index,$this->getType(),),);}foreach ($collection as $key => $value) {if (is_int($key)) {$mergedCollection[] = $value;} else {$mergedCollection[$key] = $value;}}}return $mergedCollection;}/*** @param CollectionInterface<T> $other** @throws CollectionMismatchException*/private function compareCollectionTypes(CollectionInterface $other): void{if (!$other instanceof static) {throw new CollectionMismatchException('Collection must be of type ' . static::class);}// When using generics (Collection.php, Set.php, etc),// we also need to make sure that the internal types match each otherif ($this->getUniformType($other) !== $this->getUniformType($this)) {throw new CollectionMismatchException('Collection items must be of type ' . $this->getType());}}private function getComparator(): Closure{return /*** @param T $a* @param T $b*/function (mixed $a, mixed $b): int {// If the two values are object, we convert them to unique scalars.// If the collection contains mixed values (unlikely) where some are objects// and some are not, we leave them as they are.// The comparator should still work and the result of $a < $b should// be consistent but unpredictable since not documented.if (is_object($a) && is_object($b)) {$a = spl_object_id($a);$b = spl_object_id($b);}return $a === $b ? 0 : ($a < $b ? 1 : -1);};}/*** @param CollectionInterface<mixed> $collection*/private function getUniformType(CollectionInterface $collection): string{return match ($collection->getType()) {'integer' => 'int','boolean' => 'bool','double' => 'float',default => $collection->getType(),};}}