├── composer.json ├── functions.php ├── LICENSE.txt ├── README.md ├── Iterator ├── NoChildrenIterator.php ├── NestIterator.php ├── UnfoldIterator.php ├── FilterIterator.php ├── ReplaceIterator.php ├── ExtractIterator.php ├── SortIterator.php ├── StoppableIterator.php ├── TreeIterator.php ├── TreePrinter.php ├── ZipIterator.php ├── InsertIterator.php ├── BufferedIterator.php └── MapReduce.php ├── Collection.php ├── ExtractTrait.php ├── CollectionTrait.php └── CollectionInterface.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/collection", 3 | "description": "Work easily with arrays and iterators by having a battery of utility traversal methods", 4 | "type": "library", 5 | "keywords": [ 6 | "cakephp", 7 | "collections", 8 | "iterators", 9 | "arrays" 10 | ], 11 | "homepage": "https://cakephp.org", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "CakePHP Community", 16 | "homepage": "https://github.com/cakephp/collection/graphs/contributors" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/cakephp/cakephp/issues", 21 | "forum": "https://stackoverflow.com/tags/cakephp", 22 | "irc": "irc://irc.freenode.org/cakephp", 23 | "source": "https://github.com/cakephp/collection" 24 | }, 25 | "require": { 26 | "php": ">=7.2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Cake\\Collection\\": "." 31 | }, 32 | "files": [ 33 | "functions.php" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | 1, 'b' => 2, 'c' => 3]; 17 | $collection = new Collection($items); 18 | 19 | // Create a new collection containing elements 20 | // with a value greater than one. 21 | $overOne = $collection->filter(function ($value, $key, $iterator) { 22 | return $value > 1; 23 | }); 24 | ``` 25 | 26 | The `Collection\CollectionTrait` allows you to integrate collection-like features into any Traversable object 27 | you have in your application as well. 28 | 29 | ## Documentation 30 | 31 | Please make sure you check the [official documentation](https://book.cakephp.org/4/en/core-libraries/collections.html) 32 | -------------------------------------------------------------------------------- /Iterator/NoChildrenIterator.php: -------------------------------------------------------------------------------- 1 | _nestKey = $nestKey; 47 | } 48 | 49 | /** 50 | * Returns a traversable containing the children for the current item 51 | * 52 | * @return \RecursiveIterator 53 | */ 54 | public function getChildren() 55 | { 56 | $property = $this->_propertyExtractor($this->_nestKey); 57 | 58 | return new static($property($this->current()), $this->_nestKey); 59 | } 60 | 61 | /** 62 | * Returns true if there is an array or a traversable object stored under the 63 | * configured nestKey for the current item 64 | * 65 | * @return bool 66 | */ 67 | public function hasChildren(): bool 68 | { 69 | $property = $this->_propertyExtractor($this->_nestKey); 70 | $children = $property($this->current()); 71 | 72 | if (is_array($children)) { 73 | return !empty($children); 74 | } 75 | 76 | return $children instanceof Traversable; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Iterator/UnfoldIterator.php: -------------------------------------------------------------------------------- 1 | _unfolder = $unfolder; 59 | parent::__construct($items); 60 | $this->_innerIterator = $this->getInnerIterator(); 61 | } 62 | 63 | /** 64 | * Returns true as each of the elements in the array represent a 65 | * list of items 66 | * 67 | * @return bool 68 | */ 69 | public function hasChildren(): bool 70 | { 71 | return true; 72 | } 73 | 74 | /** 75 | * Returns an iterator containing the items generated by transforming 76 | * the current value with the callable function. 77 | * 78 | * @return \RecursiveIterator 79 | */ 80 | public function getChildren() 81 | { 82 | $current = $this->current(); 83 | $key = $this->key(); 84 | $unfolder = $this->_unfolder; 85 | 86 | return new NoChildrenIterator($unfolder($current, $key, $this->_innerIterator)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Iterator/FilterIterator.php: -------------------------------------------------------------------------------- 1 | _callback = $callback; 58 | $wrapper = new CallbackFilterIterator($items, $callback); 59 | parent::__construct($wrapper); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function unwrap(): Traversable 66 | { 67 | /** @var \IteratorIterator $filter */ 68 | $filter = $this->getInnerIterator(); 69 | $iterator = $filter->getInnerIterator(); 70 | 71 | if ($iterator instanceof CollectionInterface) { 72 | $iterator = $iterator->unwrap(); 73 | } 74 | 75 | if (get_class($iterator) !== ArrayIterator::class) { 76 | return $filter; 77 | } 78 | 79 | // ArrayIterator can be traversed strictly. 80 | // Let's do that for performance gains 81 | $callback = $this->_callback; 82 | $res = []; 83 | 84 | foreach ($iterator as $k => $v) { 85 | if ($callback($v, $k, $iterator)) { 86 | $res[$k] = $v; 87 | } 88 | } 89 | 90 | return new ArrayIterator($res); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Collection.php: -------------------------------------------------------------------------------- 1 | buffered()); 55 | } 56 | 57 | /** 58 | * Unserializes the passed string and rebuilds the Collection instance 59 | * 60 | * @param string $collection The serialized collection 61 | * @return void 62 | */ 63 | public function unserialize($collection): void 64 | { 65 | $this->__construct(unserialize($collection)); 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | * 71 | * @return int 72 | */ 73 | public function count(): int 74 | { 75 | $traversable = $this->optimizeUnwrap(); 76 | 77 | if (is_array($traversable)) { 78 | return count($traversable); 79 | } 80 | 81 | return iterator_count($traversable); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | * 87 | * @return int 88 | */ 89 | public function countKeys(): int 90 | { 91 | return count($this->toArray()); 92 | } 93 | 94 | /** 95 | * Returns an array that can be used to describe the internal state of this 96 | * object. 97 | * 98 | * @return array 99 | */ 100 | public function __debugInfo(): array 101 | { 102 | return [ 103 | 'count' => $this->count(), 104 | ]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Iterator/ReplaceIterator.php: -------------------------------------------------------------------------------- 1 | _callback = $callback; 58 | parent::__construct($items); 59 | $this->_innerIterator = $this->getInnerIterator(); 60 | } 61 | 62 | /** 63 | * Returns the value returned by the callback after passing the current value in 64 | * the iteration 65 | * 66 | * @return mixed 67 | */ 68 | public function current() 69 | { 70 | $callback = $this->_callback; 71 | 72 | return $callback(parent::current(), $this->key(), $this->_innerIterator); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function unwrap(): Traversable 79 | { 80 | $iterator = $this->_innerIterator; 81 | 82 | if ($iterator instanceof CollectionInterface) { 83 | $iterator = $iterator->unwrap(); 84 | } 85 | 86 | if (get_class($iterator) !== ArrayIterator::class) { 87 | return $this; 88 | } 89 | 90 | // ArrayIterator can be traversed strictly. 91 | // Let's do that for performance gains 92 | 93 | $callback = $this->_callback; 94 | $res = []; 95 | 96 | foreach ($iterator as $k => $v) { 97 | $res[$k] = $callback($v, $k, $iterator); 98 | } 99 | 100 | return new ArrayIterator($res); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Iterator/ExtractIterator.php: -------------------------------------------------------------------------------- 1 | ['body' => 'cool', 'user' => ['name' => 'Mark']], 49 | * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] 50 | * ]; 51 | * $extractor = new ExtractIterator($items, 'comment.user.name''); 52 | * ``` 53 | * 54 | * @param iterable $items The list of values to iterate 55 | * @param string|callable $path A dot separated path of column to follow 56 | * so that the final one can be returned or a callable that will take care 57 | * of doing that. 58 | */ 59 | public function __construct(iterable $items, $path) 60 | { 61 | $this->_extractor = $this->_propertyExtractor($path); 62 | parent::__construct($items); 63 | } 64 | 65 | /** 66 | * Returns the column value defined in $path or null if the path could not be 67 | * followed 68 | * 69 | * @return mixed 70 | */ 71 | public function current() 72 | { 73 | $extractor = $this->_extractor; 74 | 75 | return $extractor(parent::current()); 76 | } 77 | 78 | /** 79 | * @inheritDoc 80 | */ 81 | public function unwrap(): Traversable 82 | { 83 | $iterator = $this->getInnerIterator(); 84 | 85 | if ($iterator instanceof CollectionInterface) { 86 | $iterator = $iterator->unwrap(); 87 | } 88 | 89 | if (get_class($iterator) !== ArrayIterator::class) { 90 | return $this; 91 | } 92 | 93 | // ArrayIterator can be traversed strictly. 94 | // Let's do that for performance gains 95 | 96 | $callback = $this->_extractor; 97 | $res = []; 98 | 99 | foreach ($iterator->getArrayCopy() as $k => $v) { 100 | $res[$k] = $callback($v); 101 | } 102 | 103 | return new ArrayIterator($res); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Iterator/SortIterator.php: -------------------------------------------------------------------------------- 1 | age; 33 | * }); 34 | * 35 | * // output all user name order by their age in descending order 36 | * foreach ($sorted as $user) { 37 | * echo $user->name; 38 | * } 39 | * ``` 40 | * 41 | * This iterator does not preserve the keys passed in the original elements. 42 | */ 43 | class SortIterator extends Collection 44 | { 45 | /** 46 | * Wraps this iterator around the passed items so when iterated they are returned 47 | * in order. 48 | * 49 | * The callback will receive as first argument each of the elements in $items, 50 | * the value returned in the callback will be used as the value for sorting such 51 | * element. Please note that the callback function could be called more than once 52 | * per element. 53 | * 54 | * @param iterable $items The values to sort 55 | * @param callable|string $callback A function used to return the actual value to 56 | * be compared. It can also be a string representing the path to use to fetch a 57 | * column or property in each element 58 | * @param int $dir either SORT_DESC or SORT_ASC 59 | * @param int $type the type of comparison to perform, either SORT_STRING 60 | * SORT_NUMERIC or SORT_NATURAL 61 | */ 62 | public function __construct(iterable $items, $callback, int $dir = \SORT_DESC, int $type = \SORT_NUMERIC) 63 | { 64 | if (!is_array($items)) { 65 | $items = iterator_to_array((new Collection($items))->unwrap(), false); 66 | } 67 | 68 | $callback = $this->_propertyExtractor($callback); 69 | $results = []; 70 | foreach ($items as $key => $val) { 71 | $val = $callback($val); 72 | if ($val instanceof DateTimeInterface && $type === \SORT_NUMERIC) { 73 | $val = $val->format('U'); 74 | } 75 | $results[$key] = $val; 76 | } 77 | 78 | $dir === SORT_DESC ? arsort($results, $type) : asort($results, $type); 79 | 80 | foreach (array_keys($results) as $key) { 81 | $results[$key] = $items[$key]; 82 | } 83 | parent::__construct($results); 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | * 89 | * @return \Traversable 90 | */ 91 | public function unwrap(): Traversable 92 | { 93 | return $this->getInnerIterator(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Iterator/StoppableIterator.php: -------------------------------------------------------------------------------- 1 | _condition = $condition; 63 | parent::__construct($items); 64 | $this->_innerIterator = $this->getInnerIterator(); 65 | } 66 | 67 | /** 68 | * Evaluates the condition and returns its result, this controls 69 | * whether or not more results will be yielded. 70 | * 71 | * @return bool 72 | */ 73 | public function valid(): bool 74 | { 75 | if (!parent::valid()) { 76 | return false; 77 | } 78 | 79 | $current = $this->current(); 80 | $key = $this->key(); 81 | $condition = $this->_condition; 82 | 83 | return !$condition($current, $key, $this->_innerIterator); 84 | } 85 | 86 | /** 87 | * @inheritDoc 88 | */ 89 | public function unwrap(): Traversable 90 | { 91 | $iterator = $this->_innerIterator; 92 | 93 | if ($iterator instanceof CollectionInterface) { 94 | $iterator = $iterator->unwrap(); 95 | } 96 | 97 | if (get_class($iterator) !== ArrayIterator::class) { 98 | return $this; 99 | } 100 | 101 | // ArrayIterator can be traversed strictly. 102 | // Let's do that for performance gains 103 | 104 | $callback = $this->_condition; 105 | $res = []; 106 | 107 | foreach ($iterator as $k => $v) { 108 | if ($callback($v, $k, $iterator)) { 109 | break; 110 | } 111 | $res[$k] = $v; 112 | } 113 | 114 | return new ArrayIterator($res); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Iterator/TreeIterator.php: -------------------------------------------------------------------------------- 1 | _mode = $mode; 53 | } 54 | 55 | /** 56 | * Returns another iterator which will return the values ready to be displayed 57 | * to a user. It does so by extracting one property from each of the elements 58 | * and prefixing it with a spacer so that the relative position in the tree 59 | * can be visualized. 60 | * 61 | * Both $valuePath and $keyPath can be a string with a property name to extract 62 | * or a dot separated path of properties that should be followed to get the last 63 | * one in the path. 64 | * 65 | * Alternatively, $valuePath and $keyPath can be callable functions. They will get 66 | * the current element as first parameter, the current iteration key as second 67 | * parameter, and the iterator instance as third argument. 68 | * 69 | * ### Example 70 | * 71 | * ``` 72 | * $printer = (new Collection($treeStructure))->listNested()->printer('name'); 73 | * ``` 74 | * 75 | * Using a closure: 76 | * 77 | * ``` 78 | * $printer = (new Collection($treeStructure)) 79 | * ->listNested() 80 | * ->printer(function ($item, $key, $iterator) { 81 | * return $item->name; 82 | * }); 83 | * ``` 84 | * 85 | * @param string|callable $valuePath The property to extract or a callable to return 86 | * the display value 87 | * @param string|callable|null $keyPath The property to use as iteration key or a 88 | * callable returning the key value. 89 | * @param string $spacer The string to use for prefixing the values according to 90 | * their depth in the tree 91 | * @return \Cake\Collection\Iterator\TreePrinter 92 | */ 93 | public function printer($valuePath, $keyPath = null, $spacer = '__') 94 | { 95 | if (!$keyPath) { 96 | $counter = 0; 97 | $keyPath = function () use (&$counter) { 98 | return $counter++; 99 | }; 100 | } 101 | 102 | return new TreePrinter( 103 | $this->getInnerIterator(), 104 | $valuePath, 105 | $keyPath, 106 | $spacer, 107 | $this->_mode 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Iterator/TreePrinter.php: -------------------------------------------------------------------------------- 1 | _value = $this->_propertyExtractor($valuePath); 81 | $this->_key = $this->_propertyExtractor($keyPath); 82 | $this->_spacer = $spacer; 83 | } 84 | 85 | /** 86 | * Returns the current iteration key 87 | * 88 | * @return mixed 89 | */ 90 | public function key() 91 | { 92 | $extractor = $this->_key; 93 | 94 | return $extractor($this->_fetchCurrent(), parent::key(), $this); 95 | } 96 | 97 | /** 98 | * Returns the current iteration value 99 | * 100 | * @return string 101 | */ 102 | public function current(): string 103 | { 104 | $extractor = $this->_value; 105 | $current = $this->_fetchCurrent(); 106 | $spacer = str_repeat($this->_spacer, $this->getDepth()); 107 | 108 | return $spacer . $extractor($current, parent::key(), $this); 109 | } 110 | 111 | /** 112 | * Advances the cursor one position 113 | * 114 | * @return void 115 | */ 116 | public function next(): void 117 | { 118 | parent::next(); 119 | $this->_current = null; 120 | } 121 | 122 | /** 123 | * Returns the current iteration element and caches its value 124 | * 125 | * @return mixed 126 | */ 127 | protected function _fetchCurrent() 128 | { 129 | if ($this->_current !== null) { 130 | return $this->_current; 131 | } 132 | 133 | return $this->_current = parent::current(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Iterator/ZipIterator.php: -------------------------------------------------------------------------------- 1 | toList(); // Returns [[1, 3], [2, 4]] 33 | * ``` 34 | * 35 | * You can also chose a custom function to zip the elements together, such 36 | * as doing a sum by index: 37 | * 38 | * ### Example 39 | * 40 | * ``` 41 | * $iterator = new ZipIterator([[1, 2], [3, 4]], function ($a, $b) { 42 | * return $a + $b; 43 | * }); 44 | * $iterator->toList(); // Returns [4, 6] 45 | * ``` 46 | */ 47 | class ZipIterator extends MultipleIterator implements CollectionInterface, Serializable 48 | { 49 | use CollectionTrait; 50 | 51 | /** 52 | * The function to use for zipping items together 53 | * 54 | * @var callable|null 55 | */ 56 | protected $_callback; 57 | 58 | /** 59 | * Contains the original iterator objects that were attached 60 | * 61 | * @var array 62 | */ 63 | protected $_iterators = []; 64 | 65 | /** 66 | * Creates the iterator to merge together the values by for all the passed 67 | * iterators by their corresponding index. 68 | * 69 | * @param array $sets The list of array or iterators to be zipped. 70 | * @param callable|null $callable The function to use for zipping the elements of each iterator. 71 | */ 72 | public function __construct(array $sets, ?callable $callable = null) 73 | { 74 | $sets = array_map(function ($items) { 75 | return (new Collection($items))->unwrap(); 76 | }, $sets); 77 | 78 | $this->_callback = $callable; 79 | parent::__construct(MultipleIterator::MIT_NEED_ALL | MultipleIterator::MIT_KEYS_NUMERIC); 80 | 81 | foreach ($sets as $set) { 82 | $this->_iterators[] = $set; 83 | $this->attachIterator($set); 84 | } 85 | } 86 | 87 | /** 88 | * Returns the value resulting out of zipping all the elements for all the 89 | * iterators with the same positional index. 90 | * 91 | * @return array 92 | */ 93 | public function current() 94 | { 95 | if ($this->_callback === null) { 96 | return parent::current(); 97 | } 98 | 99 | return call_user_func_array($this->_callback, parent::current()); 100 | } 101 | 102 | /** 103 | * Returns a string representation of this object that can be used 104 | * to reconstruct it 105 | * 106 | * @return string 107 | */ 108 | public function serialize(): string 109 | { 110 | return serialize($this->_iterators); 111 | } 112 | 113 | /** 114 | * Unserializes the passed string and rebuilds the ZipIterator instance 115 | * 116 | * @param string $iterators The serialized iterators 117 | * @return void 118 | */ 119 | public function unserialize($iterators): void 120 | { 121 | parent::__construct(MultipleIterator::MIT_NEED_ALL | MultipleIterator::MIT_KEYS_NUMERIC); 122 | $this->_iterators = unserialize($iterators); 123 | foreach ($this->_iterators as $it) { 124 | $this->attachIterator($it); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Iterator/InsertIterator.php: -------------------------------------------------------------------------------- 1 | _path = $path; 80 | $this->_target = $target; 81 | $this->_values = $values; 82 | } 83 | 84 | /** 85 | * Advances the cursor to the next record 86 | * 87 | * @return void 88 | */ 89 | public function next(): void 90 | { 91 | parent::next(); 92 | if ($this->_validValues) { 93 | $this->_values->next(); 94 | } 95 | $this->_validValues = $this->_values->valid(); 96 | } 97 | 98 | /** 99 | * Returns the current element in the target collection after inserting 100 | * the value from the source collection into the specified path. 101 | * 102 | * @return mixed 103 | */ 104 | public function current() 105 | { 106 | $row = parent::current(); 107 | 108 | if (!$this->_validValues) { 109 | return $row; 110 | } 111 | 112 | $pointer = &$row; 113 | foreach ($this->_path as $step) { 114 | if (!isset($pointer[$step])) { 115 | return $row; 116 | } 117 | $pointer = &$pointer[$step]; 118 | } 119 | 120 | $pointer[$this->_target] = $this->_values->current(); 121 | 122 | return $row; 123 | } 124 | 125 | /** 126 | * Resets the collection pointer. 127 | * 128 | * @return void 129 | */ 130 | public function rewind(): void 131 | { 132 | parent::rewind(); 133 | $this->_values->rewind(); 134 | $this->_validValues = $this->_values->valid(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /ExtractTrait.php: -------------------------------------------------------------------------------- 1 | _extract($element, $parts); 48 | }; 49 | } 50 | 51 | return function ($element) use ($parts) { 52 | return $this->_simpleExtract($element, $parts); 53 | }; 54 | } 55 | 56 | /** 57 | * Returns a column from $data that can be extracted 58 | * by iterating over the column names contained in $path. 59 | * It will return arrays for elements in represented with `{*}` 60 | * 61 | * @param array|\ArrayAccess $data Data. 62 | * @param string[] $parts Path to extract from. 63 | * @return mixed 64 | */ 65 | protected function _extract($data, array $parts) 66 | { 67 | $value = null; 68 | $collectionTransform = false; 69 | 70 | foreach ($parts as $i => $column) { 71 | if ($column === '{*}') { 72 | $collectionTransform = true; 73 | continue; 74 | } 75 | 76 | if ( 77 | $collectionTransform && 78 | !( 79 | $data instanceof Traversable || 80 | is_array($data) 81 | ) 82 | ) { 83 | return null; 84 | } 85 | 86 | if ($collectionTransform) { 87 | $rest = implode('.', array_slice($parts, $i)); 88 | 89 | return (new Collection($data))->extract($rest); 90 | } 91 | 92 | if (!isset($data[$column])) { 93 | return null; 94 | } 95 | 96 | $value = $data[$column]; 97 | $data = $value; 98 | } 99 | 100 | return $value; 101 | } 102 | 103 | /** 104 | * Returns a column from $data that can be extracted 105 | * by iterating over the column names contained in $path 106 | * 107 | * @param array|\ArrayAccess $data Data. 108 | * @param string[] $parts Path to extract from. 109 | * @return mixed 110 | */ 111 | protected function _simpleExtract($data, array $parts) 112 | { 113 | $value = null; 114 | foreach ($parts as $column) { 115 | if (!isset($data[$column])) { 116 | return null; 117 | } 118 | $value = $data[$column]; 119 | $data = $value; 120 | } 121 | 122 | return $value; 123 | } 124 | 125 | /** 126 | * Returns a callable that receives a value and will return whether or not 127 | * it matches certain condition. 128 | * 129 | * @param array $conditions A key-value list of conditions to match where the 130 | * key is the property path to get from the current item and the value is the 131 | * value to be compared the item with. 132 | * @return \Closure 133 | */ 134 | protected function _createMatcherFilter(array $conditions): Closure 135 | { 136 | $matchers = []; 137 | foreach ($conditions as $property => $value) { 138 | $extractor = $this->_propertyExtractor($property); 139 | $matchers[] = function ($v) use ($extractor, $value) { 140 | return $extractor($v) == $value; 141 | }; 142 | } 143 | 144 | return function ($value) use ($matchers) { 145 | foreach ($matchers as $match) { 146 | if (!$match($value)) { 147 | return false; 148 | } 149 | } 150 | 151 | return true; 152 | }; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Iterator/BufferedIterator.php: -------------------------------------------------------------------------------- 1 | _buffer = new SplDoublyLinkedList(); 82 | parent::__construct($items); 83 | } 84 | 85 | /** 86 | * Returns the current key in the iterator 87 | * 88 | * @return mixed 89 | */ 90 | public function key() 91 | { 92 | return $this->_key; 93 | } 94 | 95 | /** 96 | * Returns the current record in the iterator 97 | * 98 | * @return mixed 99 | */ 100 | public function current() 101 | { 102 | return $this->_current; 103 | } 104 | 105 | /** 106 | * Rewinds the collection 107 | * 108 | * @return void 109 | */ 110 | public function rewind(): void 111 | { 112 | if ($this->_index === 0 && !$this->_started) { 113 | $this->_started = true; 114 | parent::rewind(); 115 | 116 | return; 117 | } 118 | 119 | $this->_index = 0; 120 | } 121 | 122 | /** 123 | * Returns whether or not the iterator has more elements 124 | * 125 | * @return bool 126 | */ 127 | public function valid(): bool 128 | { 129 | if ($this->_buffer->offsetExists($this->_index)) { 130 | $current = $this->_buffer->offsetGet($this->_index); 131 | $this->_current = $current['value']; 132 | $this->_key = $current['key']; 133 | 134 | return true; 135 | } 136 | 137 | $valid = parent::valid(); 138 | 139 | if ($valid) { 140 | $this->_current = parent::current(); 141 | $this->_key = parent::key(); 142 | $this->_buffer->push([ 143 | 'key' => $this->_key, 144 | 'value' => $this->_current, 145 | ]); 146 | } 147 | 148 | $this->_finished = !$valid; 149 | 150 | return $valid; 151 | } 152 | 153 | /** 154 | * Advances the iterator pointer to the next element 155 | * 156 | * @return void 157 | */ 158 | public function next(): void 159 | { 160 | $this->_index++; 161 | 162 | // Don't move inner iterator if we have more buffer 163 | if ($this->_buffer->offsetExists($this->_index)) { 164 | return; 165 | } 166 | if (!$this->_finished) { 167 | parent::next(); 168 | } 169 | } 170 | 171 | /** 172 | * Returns the number or items in this collection 173 | * 174 | * @return int 175 | */ 176 | public function count(): int 177 | { 178 | if (!$this->_started) { 179 | $this->rewind(); 180 | } 181 | 182 | while ($this->valid()) { 183 | $this->next(); 184 | } 185 | 186 | return $this->_buffer->count(); 187 | } 188 | 189 | /** 190 | * Returns a string representation of this object that can be used 191 | * to reconstruct it 192 | * 193 | * @return string 194 | */ 195 | public function serialize(): string 196 | { 197 | if (!$this->_finished) { 198 | $this->count(); 199 | } 200 | 201 | return serialize($this->_buffer); 202 | } 203 | 204 | /** 205 | * Unserializes the passed string and rebuilds the BufferedIterator instance 206 | * 207 | * @param string $collection The serialized buffer iterator 208 | * @return void 209 | */ 210 | public function unserialize($collection): void 211 | { 212 | $this->__construct([]); 213 | $this->_buffer = unserialize($collection); 214 | $this->_started = true; 215 | $this->_finished = true; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Iterator/MapReduce.php: -------------------------------------------------------------------------------- 1 | emitIntermediate($value, $type); 94 | * }; 95 | * 96 | * $reducer = function ($numbers, $type, $mr) { 97 | * $mr->emit(array_unique($numbers), $type); 98 | * }; 99 | * $results = new MapReduce($data, $mapper, $reducer); 100 | * ``` 101 | * 102 | * Previous example will generate the following result: 103 | * 104 | * ``` 105 | * ['odd' => [1, 3, 5], 'even' => [2, 4]] 106 | * ``` 107 | * 108 | * @param \Traversable $data the original data to be processed 109 | * @param callable $mapper the mapper callback. This function will receive 3 arguments. 110 | * The first one is the current value, second the current results key and third is 111 | * this class instance so you can call the result emitters. 112 | * @param callable|null $reducer the reducer callback. This function will receive 3 arguments. 113 | * The first one is the list of values inside a bucket, second one is the name 114 | * of the bucket that was created during the mapping phase and third one is an 115 | * instance of this class. 116 | */ 117 | public function __construct(Traversable $data, callable $mapper, ?callable $reducer = null) 118 | { 119 | $this->_data = $data; 120 | $this->_mapper = $mapper; 121 | $this->_reducer = $reducer; 122 | } 123 | 124 | /** 125 | * Returns an iterator with the end result of running the Map and Reduce 126 | * phases on the original data 127 | * 128 | * @return \Traversable 129 | */ 130 | public function getIterator(): Traversable 131 | { 132 | if (!$this->_executed) { 133 | $this->_execute(); 134 | } 135 | 136 | return new ArrayIterator($this->_result); 137 | } 138 | 139 | /** 140 | * Appends a new record to the bucket labelled with $key, usually as a result 141 | * of mapping a single record from the original data. 142 | * 143 | * @param mixed $val The record itself to store in the bucket 144 | * @param mixed $bucket the name of the bucket where to put the record 145 | * @return void 146 | */ 147 | public function emitIntermediate($val, $bucket): void 148 | { 149 | $this->_intermediate[$bucket][] = $val; 150 | } 151 | 152 | /** 153 | * Appends a new record to the final list of results and optionally assign a key 154 | * for this record. 155 | * 156 | * @param mixed $val The value to be appended to the final list of results 157 | * @param mixed $key and optional key to assign to the value 158 | * @return void 159 | */ 160 | public function emit($val, $key = null): void 161 | { 162 | $this->_result[$key ?? $this->_counter] = $val; 163 | $this->_counter++; 164 | } 165 | 166 | /** 167 | * Runs the actual Map-Reduce algorithm. This is iterate the original data 168 | * and call the mapper function for each , then for each intermediate 169 | * bucket created during the Map phase call the reduce function. 170 | * 171 | * @return void 172 | * @throws \LogicException if emitIntermediate was called but no reducer function 173 | * was provided 174 | */ 175 | protected function _execute(): void 176 | { 177 | $mapper = $this->_mapper; 178 | foreach ($this->_data as $key => $val) { 179 | $mapper($val, $key, $this); 180 | } 181 | 182 | if (!empty($this->_intermediate) && empty($this->_reducer)) { 183 | throw new LogicException('No reducer function was provided'); 184 | } 185 | 186 | /** @var callable $reducer */ 187 | $reducer = $this->_reducer; 188 | foreach ($this->_intermediate as $key => $list) { 189 | $reducer($list, $key, $this); 190 | } 191 | $this->_intermediate = []; 192 | $this->_executed = true; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /CollectionTrait.php: -------------------------------------------------------------------------------- 1 | optimizeUnwrap() as $k => $v) { 69 | $callback($v, $k); 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function filter(?callable $callback = null): CollectionInterface 79 | { 80 | if ($callback === null) { 81 | $callback = function ($v) { 82 | return (bool)$v; 83 | }; 84 | } 85 | 86 | return new FilterIterator($this->unwrap(), $callback); 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function reject(callable $callback): CollectionInterface 93 | { 94 | return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($callback) { 95 | return !$callback($key, $value, $items); 96 | }); 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | public function every(callable $callback): bool 103 | { 104 | foreach ($this->optimizeUnwrap() as $key => $value) { 105 | if (!$callback($value, $key)) { 106 | return false; 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * @inheritDoc 115 | */ 116 | public function some(callable $callback): bool 117 | { 118 | foreach ($this->optimizeUnwrap() as $key => $value) { 119 | if ($callback($value, $key) === true) { 120 | return true; 121 | } 122 | } 123 | 124 | return false; 125 | } 126 | 127 | /** 128 | * @inheritDoc 129 | */ 130 | public function contains($value): bool 131 | { 132 | foreach ($this->optimizeUnwrap() as $v) { 133 | if ($value === $v) { 134 | return true; 135 | } 136 | } 137 | 138 | return false; 139 | } 140 | 141 | /** 142 | * @inheritDoc 143 | */ 144 | public function map(callable $callback): CollectionInterface 145 | { 146 | return new ReplaceIterator($this->unwrap(), $callback); 147 | } 148 | 149 | /** 150 | * @inheritDoc 151 | */ 152 | public function reduce(callable $callback, $initial = null) 153 | { 154 | $isFirst = false; 155 | if (func_num_args() < 2) { 156 | $isFirst = true; 157 | } 158 | 159 | $result = $initial; 160 | foreach ($this->optimizeUnwrap() as $k => $value) { 161 | if ($isFirst) { 162 | $result = $value; 163 | $isFirst = false; 164 | continue; 165 | } 166 | $result = $callback($result, $value, $k); 167 | } 168 | 169 | return $result; 170 | } 171 | 172 | /** 173 | * @inheritDoc 174 | */ 175 | public function extract($path): CollectionInterface 176 | { 177 | $extractor = new ExtractIterator($this->unwrap(), $path); 178 | if (is_string($path) && strpos($path, '{*}') !== false) { 179 | $extractor = $extractor 180 | ->filter(function ($data) { 181 | return $data !== null && ($data instanceof Traversable || is_array($data)); 182 | }) 183 | ->unfold(); 184 | } 185 | 186 | return $extractor; 187 | } 188 | 189 | /** 190 | * @inheritDoc 191 | */ 192 | public function max($path, int $sort = \SORT_NUMERIC) 193 | { 194 | return (new SortIterator($this->unwrap(), $path, \SORT_DESC, $sort))->first(); 195 | } 196 | 197 | /** 198 | * @inheritDoc 199 | */ 200 | public function min($path, int $sort = \SORT_NUMERIC) 201 | { 202 | return (new SortIterator($this->unwrap(), $path, \SORT_ASC, $sort))->first(); 203 | } 204 | 205 | /** 206 | * @inheritDoc 207 | */ 208 | public function avg($path = null) 209 | { 210 | $result = $this; 211 | if ($path !== null) { 212 | $result = $result->extract($path); 213 | } 214 | $result = $result 215 | ->reduce(function ($acc, $current) { 216 | [$count, $sum] = $acc; 217 | 218 | return [$count + 1, $sum + $current]; 219 | }, [0, 0]); 220 | 221 | if ($result[0] === 0) { 222 | return null; 223 | } 224 | 225 | return $result[1] / $result[0]; 226 | } 227 | 228 | /** 229 | * @inheritDoc 230 | */ 231 | public function median($path = null) 232 | { 233 | $items = $this; 234 | if ($path !== null) { 235 | $items = $items->extract($path); 236 | } 237 | $values = $items->toList(); 238 | sort($values); 239 | $count = count($values); 240 | 241 | if ($count === 0) { 242 | return null; 243 | } 244 | 245 | $middle = (int)($count / 2); 246 | 247 | if ($count % 2) { 248 | return $values[$middle]; 249 | } 250 | 251 | return ($values[$middle - 1] + $values[$middle]) / 2; 252 | } 253 | 254 | /** 255 | * @inheritDoc 256 | */ 257 | public function sortBy($path, int $order = \SORT_DESC, int $sort = \SORT_NUMERIC): CollectionInterface 258 | { 259 | return new SortIterator($this->unwrap(), $path, $order, $sort); 260 | } 261 | 262 | /** 263 | * @inheritDoc 264 | */ 265 | public function groupBy($path): CollectionInterface 266 | { 267 | $callback = $this->_propertyExtractor($path); 268 | $group = []; 269 | foreach ($this->optimizeUnwrap() as $value) { 270 | $pathValue = $callback($value); 271 | if ($pathValue === null) { 272 | throw new InvalidArgumentException( 273 | 'Cannot group by path that does not exist or contains a null value. ' . 274 | 'Use a callback to return a default value for that path.' 275 | ); 276 | } 277 | $group[$pathValue][] = $value; 278 | } 279 | 280 | return $this->newCollection($group); 281 | } 282 | 283 | /** 284 | * @inheritDoc 285 | */ 286 | public function indexBy($path): CollectionInterface 287 | { 288 | $callback = $this->_propertyExtractor($path); 289 | $group = []; 290 | foreach ($this->optimizeUnwrap() as $value) { 291 | $pathValue = $callback($value); 292 | if ($pathValue === null) { 293 | throw new InvalidArgumentException( 294 | 'Cannot index by path that does not exist or contains a null value. ' . 295 | 'Use a callback to return a default value for that path.' 296 | ); 297 | } 298 | $group[$pathValue] = $value; 299 | } 300 | 301 | return $this->newCollection($group); 302 | } 303 | 304 | /** 305 | * @inheritDoc 306 | */ 307 | public function countBy($path): CollectionInterface 308 | { 309 | $callback = $this->_propertyExtractor($path); 310 | 311 | $mapper = function ($value, $key, $mr) use ($callback): void { 312 | /** @var \Cake\Collection\Iterator\MapReduce $mr */ 313 | $mr->emitIntermediate($value, $callback($value)); 314 | }; 315 | 316 | $reducer = function ($values, $key, $mr): void { 317 | /** @var \Cake\Collection\Iterator\MapReduce $mr */ 318 | $mr->emit(count($values), $key); 319 | }; 320 | 321 | return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)); 322 | } 323 | 324 | /** 325 | * @inheritDoc 326 | */ 327 | public function sumOf($path = null) 328 | { 329 | if ($path === null) { 330 | return array_sum($this->toList()); 331 | } 332 | 333 | $callback = $this->_propertyExtractor($path); 334 | $sum = 0; 335 | foreach ($this->optimizeUnwrap() as $k => $v) { 336 | $sum += $callback($v, $k); 337 | } 338 | 339 | return $sum; 340 | } 341 | 342 | /** 343 | * @inheritDoc 344 | */ 345 | public function shuffle(): CollectionInterface 346 | { 347 | $items = $this->toList(); 348 | shuffle($items); 349 | 350 | return $this->newCollection($items); 351 | } 352 | 353 | /** 354 | * @inheritDoc 355 | */ 356 | public function sample(int $length = 10): CollectionInterface 357 | { 358 | return $this->newCollection(new LimitIterator($this->shuffle(), 0, $length)); 359 | } 360 | 361 | /** 362 | * @inheritDoc 363 | */ 364 | public function take(int $length = 1, int $offset = 0): CollectionInterface 365 | { 366 | return $this->newCollection(new LimitIterator($this, $offset, $length)); 367 | } 368 | 369 | /** 370 | * @inheritDoc 371 | */ 372 | public function skip(int $length): CollectionInterface 373 | { 374 | return $this->newCollection(new LimitIterator($this, $length)); 375 | } 376 | 377 | /** 378 | * @inheritDoc 379 | */ 380 | public function match(array $conditions): CollectionInterface 381 | { 382 | return $this->filter($this->_createMatcherFilter($conditions)); 383 | } 384 | 385 | /** 386 | * @inheritDoc 387 | */ 388 | public function firstMatch(array $conditions) 389 | { 390 | return $this->match($conditions)->first(); 391 | } 392 | 393 | /** 394 | * @inheritDoc 395 | */ 396 | public function first() 397 | { 398 | $iterator = new LimitIterator($this, 0, 1); 399 | foreach ($iterator as $result) { 400 | return $result; 401 | } 402 | } 403 | 404 | /** 405 | * @inheritDoc 406 | */ 407 | public function last() 408 | { 409 | $iterator = $this->optimizeUnwrap(); 410 | if (is_array($iterator)) { 411 | return array_pop($iterator); 412 | } 413 | 414 | if ($iterator instanceof Countable) { 415 | $count = count($iterator); 416 | if ($count === 0) { 417 | return null; 418 | } 419 | /** @var iterable $iterator */ 420 | $iterator = new LimitIterator($iterator, $count - 1, 1); 421 | } 422 | 423 | $result = null; 424 | foreach ($iterator as $result) { 425 | // No-op 426 | } 427 | 428 | return $result; 429 | } 430 | 431 | /** 432 | * @inheritDoc 433 | */ 434 | public function takeLast(int $length): CollectionInterface 435 | { 436 | if ($length < 1) { 437 | throw new InvalidArgumentException('The takeLast method requires a number greater than 0.'); 438 | } 439 | 440 | $iterator = $this->optimizeUnwrap(); 441 | if (is_array($iterator)) { 442 | return $this->newCollection(array_slice($iterator, $length * -1)); 443 | } 444 | 445 | if ($iterator instanceof Countable) { 446 | $count = count($iterator); 447 | 448 | if ($count === 0) { 449 | return $this->newCollection([]); 450 | } 451 | 452 | $iterator = new LimitIterator($iterator, max(0, $count - $length), $length); 453 | 454 | return $this->newCollection($iterator); 455 | } 456 | 457 | $generator = function ($iterator, $length) { 458 | $result = []; 459 | $bucket = 0; 460 | $offset = 0; 461 | 462 | /** 463 | * Consider the collection of elements [1, 2, 3, 4, 5, 6, 7, 8, 9], in order 464 | * to get the last 4 elements, we can keep a buffer of 4 elements and 465 | * fill it circularly using modulo logic, we use the $bucket variable 466 | * to track the position to fill next in the buffer. This how the buffer 467 | * looks like after 4 iterations: 468 | * 469 | * 0) 1 2 3 4 -- $bucket now goes back to 0, we have filled 4 elementes 470 | * 1) 5 2 3 4 -- 5th iteration 471 | * 2) 5 6 3 4 -- 6th iteration 472 | * 3) 5 6 7 4 -- 7th iteration 473 | * 4) 5 6 7 8 -- 8th iteration 474 | * 5) 9 6 7 8 475 | * 476 | * We can see that at the end of the iterations, the buffer contains all 477 | * the last four elements, just in the wrong order. How do we keep the 478 | * original order? Well, it turns out that the number of iteration also 479 | * give us a clue on what's going on, Let's add a marker for it now: 480 | * 481 | * 0) 1 2 3 4 482 | * ^ -- The 0) above now becomes the $offset variable 483 | * 1) 5 2 3 4 484 | * ^ -- $offset = 1 485 | * 2) 5 6 3 4 486 | * ^ -- $offset = 2 487 | * 3) 5 6 7 4 488 | * ^ -- $offset = 3 489 | * 4) 5 6 7 8 490 | * ^ -- We use module logic for $offset too 491 | * and as you can see each time $offset is 0, then the buffer 492 | * is sorted exactly as we need. 493 | * 5) 9 6 7 8 494 | * ^ -- $offset = 1 495 | * 496 | * The $offset variable is a marker for splitting the buffer in two, 497 | * elements to the right for the marker are the head of the final result, 498 | * whereas the elements at the left are the tail. For example consider step 5) 499 | * which has an offset of 1: 500 | * 501 | * - $head = elements to the right = [6, 7, 8] 502 | * - $tail = elements to the left = [9] 503 | * - $result = $head + $tail = [6, 7, 8, 9] 504 | * 505 | * The logic above applies to collections of any size. 506 | */ 507 | 508 | foreach ($iterator as $k => $item) { 509 | $result[$bucket] = [$k, $item]; 510 | $bucket = (++$bucket) % $length; 511 | $offset++; 512 | } 513 | 514 | $offset = $offset % $length; 515 | $head = array_slice($result, $offset); 516 | $tail = array_slice($result, 0, $offset); 517 | 518 | foreach ($head as $v) { 519 | yield $v[0] => $v[1]; 520 | } 521 | 522 | foreach ($tail as $v) { 523 | yield $v[0] => $v[1]; 524 | } 525 | }; 526 | 527 | return $this->newCollection($generator($iterator, $length)); 528 | } 529 | 530 | /** 531 | * @inheritDoc 532 | */ 533 | public function append($items): CollectionInterface 534 | { 535 | $list = new AppendIterator(); 536 | $list->append($this->unwrap()); 537 | $list->append($this->newCollection($items)->unwrap()); 538 | 539 | return $this->newCollection($list); 540 | } 541 | 542 | /** 543 | * @inheritDoc 544 | */ 545 | public function appendItem($item, $key = null): CollectionInterface 546 | { 547 | if ($key !== null) { 548 | $data = [$key => $item]; 549 | } else { 550 | $data = [$item]; 551 | } 552 | 553 | return $this->append($data); 554 | } 555 | 556 | /** 557 | * @inheritDoc 558 | */ 559 | public function prepend($items): CollectionInterface 560 | { 561 | return $this->newCollection($items)->append($this); 562 | } 563 | 564 | /** 565 | * @inheritDoc 566 | */ 567 | public function prependItem($item, $key = null): CollectionInterface 568 | { 569 | if ($key !== null) { 570 | $data = [$key => $item]; 571 | } else { 572 | $data = [$item]; 573 | } 574 | 575 | return $this->prepend($data); 576 | } 577 | 578 | /** 579 | * @inheritDoc 580 | */ 581 | public function combine($keyPath, $valuePath, $groupPath = null): CollectionInterface 582 | { 583 | $options = [ 584 | 'keyPath' => $this->_propertyExtractor($keyPath), 585 | 'valuePath' => $this->_propertyExtractor($valuePath), 586 | 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null, 587 | ]; 588 | 589 | $mapper = function ($value, $key, MapReduce $mapReduce) use ($options) { 590 | $rowKey = $options['keyPath']; 591 | $rowVal = $options['valuePath']; 592 | 593 | if (!$options['groupPath']) { 594 | $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key)); 595 | 596 | return null; 597 | } 598 | 599 | $key = $options['groupPath']($value, $key); 600 | $mapReduce->emitIntermediate( 601 | [$rowKey($value, $key) => $rowVal($value, $key)], 602 | $key 603 | ); 604 | }; 605 | 606 | $reducer = function ($values, $key, MapReduce $mapReduce): void { 607 | $result = []; 608 | foreach ($values as $value) { 609 | $result += $value; 610 | } 611 | $mapReduce->emit($result, $key); 612 | }; 613 | 614 | return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)); 615 | } 616 | 617 | /** 618 | * @inheritDoc 619 | */ 620 | public function nest($idPath, $parentPath, string $nestingKey = 'children'): CollectionInterface 621 | { 622 | $parents = []; 623 | $idPath = $this->_propertyExtractor($idPath); 624 | $parentPath = $this->_propertyExtractor($parentPath); 625 | $isObject = true; 626 | 627 | $mapper = function ($row, $key, MapReduce $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey): void { 628 | $row[$nestingKey] = []; 629 | $id = $idPath($row, $key); 630 | $parentId = $parentPath($row, $key); 631 | $parents[$id] = &$row; 632 | $mapReduce->emitIntermediate($id, $parentId); 633 | }; 634 | 635 | $reducer = function ($values, $key, MapReduce $mapReduce) use (&$parents, &$isObject, $nestingKey) { 636 | static $foundOutType = false; 637 | if (!$foundOutType) { 638 | $isObject = is_object(current($parents)); 639 | $foundOutType = true; 640 | } 641 | if (empty($key) || !isset($parents[$key])) { 642 | foreach ($values as $id) { 643 | /** @psalm-suppress PossiblyInvalidArgument */ 644 | $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1); 645 | $mapReduce->emit($parents[$id]); 646 | } 647 | 648 | return null; 649 | } 650 | 651 | $children = []; 652 | foreach ($values as $id) { 653 | $children[] = &$parents[$id]; 654 | } 655 | $parents[$key][$nestingKey] = $children; 656 | }; 657 | 658 | return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)) 659 | ->map(function ($value) use (&$isObject) { 660 | /** @var \ArrayIterator $value */ 661 | return $isObject ? $value : $value->getArrayCopy(); 662 | }); 663 | } 664 | 665 | /** 666 | * @inheritDoc 667 | */ 668 | public function insert(string $path, $values): CollectionInterface 669 | { 670 | return new InsertIterator($this->unwrap(), $path, $values); 671 | } 672 | 673 | /** 674 | * @inheritDoc 675 | */ 676 | public function toArray(bool $preserveKeys = true): array 677 | { 678 | $iterator = $this->unwrap(); 679 | if ($iterator instanceof ArrayIterator) { 680 | $items = $iterator->getArrayCopy(); 681 | 682 | return $preserveKeys ? $items : array_values($items); 683 | } 684 | // RecursiveIteratorIterator can return duplicate key values causing 685 | // data loss when converted into an array 686 | if ($preserveKeys && get_class($iterator) === RecursiveIteratorIterator::class) { 687 | $preserveKeys = false; 688 | } 689 | 690 | return iterator_to_array($this, $preserveKeys); 691 | } 692 | 693 | /** 694 | * @inheritDoc 695 | */ 696 | public function toList(): array 697 | { 698 | return $this->toArray(false); 699 | } 700 | 701 | /** 702 | * @inheritDoc 703 | */ 704 | public function jsonSerialize(): array 705 | { 706 | return $this->toArray(); 707 | } 708 | 709 | /** 710 | * @inheritDoc 711 | */ 712 | public function compile(bool $preserveKeys = true): CollectionInterface 713 | { 714 | return $this->newCollection($this->toArray($preserveKeys)); 715 | } 716 | 717 | /** 718 | * @inheritDoc 719 | */ 720 | public function lazy(): CollectionInterface 721 | { 722 | $generator = function () { 723 | foreach ($this->unwrap() as $k => $v) { 724 | yield $k => $v; 725 | } 726 | }; 727 | 728 | return $this->newCollection($generator()); 729 | } 730 | 731 | /** 732 | * @inheritDoc 733 | */ 734 | public function buffered(): CollectionInterface 735 | { 736 | return new BufferedIterator($this->unwrap()); 737 | } 738 | 739 | /** 740 | * @inheritDoc 741 | */ 742 | public function listNested($order = 'desc', $nestingKey = 'children'): CollectionInterface 743 | { 744 | if (is_string($order)) { 745 | $order = strtolower($order); 746 | $modes = [ 747 | 'desc' => RecursiveIteratorIterator::SELF_FIRST, 748 | 'asc' => RecursiveIteratorIterator::CHILD_FIRST, 749 | 'leaves' => RecursiveIteratorIterator::LEAVES_ONLY, 750 | ]; 751 | 752 | if (!isset($modes[$order])) { 753 | throw new RuntimeException(sprintf( 754 | "Invalid direction `%s` provided. Must be one of: 'desc', 'asc', 'leaves'", 755 | $order 756 | )); 757 | } 758 | $order = $modes[$order]; 759 | } 760 | 761 | return new TreeIterator( 762 | new NestIterator($this, $nestingKey), 763 | $order 764 | ); 765 | } 766 | 767 | /** 768 | * @inheritDoc 769 | */ 770 | public function stopWhen($condition): CollectionInterface 771 | { 772 | if (!is_callable($condition)) { 773 | $condition = $this->_createMatcherFilter($condition); 774 | } 775 | 776 | return new StoppableIterator($this->unwrap(), $condition); 777 | } 778 | 779 | /** 780 | * @inheritDoc 781 | */ 782 | public function unfold(?callable $callback = null): CollectionInterface 783 | { 784 | if ($callback === null) { 785 | $callback = function ($item) { 786 | return $item; 787 | }; 788 | } 789 | 790 | return $this->newCollection( 791 | new RecursiveIteratorIterator( 792 | new UnfoldIterator($this->unwrap(), $callback), 793 | RecursiveIteratorIterator::LEAVES_ONLY 794 | ) 795 | ); 796 | } 797 | 798 | /** 799 | * @inheritDoc 800 | */ 801 | public function through(callable $callback): CollectionInterface 802 | { 803 | $result = $callback($this); 804 | 805 | return $result instanceof CollectionInterface ? $result : $this->newCollection($result); 806 | } 807 | 808 | /** 809 | * @inheritDoc 810 | */ 811 | public function zip(iterable $items): CollectionInterface 812 | { 813 | return new ZipIterator(array_merge([$this->unwrap()], func_get_args())); 814 | } 815 | 816 | /** 817 | * @inheritDoc 818 | */ 819 | public function zipWith(iterable $items, $callback): CollectionInterface 820 | { 821 | if (func_num_args() > 2) { 822 | $items = func_get_args(); 823 | $callback = array_pop($items); 824 | } else { 825 | $items = [$items]; 826 | } 827 | 828 | return new ZipIterator(array_merge([$this->unwrap()], $items), $callback); 829 | } 830 | 831 | /** 832 | * @inheritDoc 833 | */ 834 | public function chunk(int $chunkSize): CollectionInterface 835 | { 836 | return $this->map(function ($v, $k, $iterator) use ($chunkSize) { 837 | $values = [$v]; 838 | for ($i = 1; $i < $chunkSize; $i++) { 839 | $iterator->next(); 840 | if (!$iterator->valid()) { 841 | break; 842 | } 843 | $values[] = $iterator->current(); 844 | } 845 | 846 | return $values; 847 | }); 848 | } 849 | 850 | /** 851 | * @inheritDoc 852 | */ 853 | public function chunkWithKeys(int $chunkSize, bool $preserveKeys = true): CollectionInterface 854 | { 855 | return $this->map(function ($v, $k, $iterator) use ($chunkSize, $preserveKeys) { 856 | $key = 0; 857 | if ($preserveKeys) { 858 | $key = $k; 859 | } 860 | $values = [$key => $v]; 861 | for ($i = 1; $i < $chunkSize; $i++) { 862 | $iterator->next(); 863 | if (!$iterator->valid()) { 864 | break; 865 | } 866 | if ($preserveKeys) { 867 | $values[$iterator->key()] = $iterator->current(); 868 | } else { 869 | $values[] = $iterator->current(); 870 | } 871 | } 872 | 873 | return $values; 874 | }); 875 | } 876 | 877 | /** 878 | * @inheritDoc 879 | */ 880 | public function isEmpty(): bool 881 | { 882 | foreach ($this as $el) { 883 | return false; 884 | } 885 | 886 | return true; 887 | } 888 | 889 | /** 890 | * @inheritDoc 891 | */ 892 | public function unwrap(): Traversable 893 | { 894 | $iterator = $this; 895 | while ( 896 | get_class($iterator) === Collection::class 897 | && $iterator instanceof OuterIterator 898 | ) { 899 | $iterator = $iterator->getInnerIterator(); 900 | } 901 | 902 | if ($iterator !== $this && $iterator instanceof CollectionInterface) { 903 | $iterator = $iterator->unwrap(); 904 | } 905 | 906 | return $iterator; 907 | } 908 | 909 | /** 910 | * {@inheritDoc} 911 | * 912 | * @param callable|null $operation A callable that allows you to customize the product result. 913 | * @param callable|null $filter A filtering callback that must return true for a result to be part 914 | * of the final results. 915 | * @return \Cake\Collection\CollectionInterface 916 | * @throws \LogicException 917 | */ 918 | public function cartesianProduct(?callable $operation = null, ?callable $filter = null): CollectionInterface 919 | { 920 | if ($this->isEmpty()) { 921 | return $this->newCollection([]); 922 | } 923 | 924 | $collectionArrays = []; 925 | $collectionArraysKeys = []; 926 | $collectionArraysCounts = []; 927 | 928 | foreach ($this->toList() as $value) { 929 | $valueCount = count($value); 930 | if ($valueCount !== count($value, COUNT_RECURSIVE)) { 931 | throw new LogicException('Cannot find the cartesian product of a multidimensional array'); 932 | } 933 | 934 | $collectionArraysKeys[] = array_keys($value); 935 | $collectionArraysCounts[] = $valueCount; 936 | $collectionArrays[] = $value; 937 | } 938 | 939 | $result = []; 940 | $lastIndex = count($collectionArrays) - 1; 941 | // holds the indexes of the arrays that generate the current combination 942 | $currentIndexes = array_fill(0, $lastIndex + 1, 0); 943 | 944 | $changeIndex = $lastIndex; 945 | 946 | while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) { 947 | $currentCombination = array_map(function ($value, $keys, $index) { 948 | return $value[$keys[$index]]; 949 | }, $collectionArrays, $collectionArraysKeys, $currentIndexes); 950 | 951 | if ($filter === null || $filter($currentCombination)) { 952 | $result[] = $operation === null ? $currentCombination : $operation($currentCombination); 953 | } 954 | 955 | $currentIndexes[$lastIndex]++; 956 | 957 | for ( 958 | $changeIndex = $lastIndex; 959 | $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0; 960 | $changeIndex-- 961 | ) { 962 | $currentIndexes[$changeIndex] = 0; 963 | $currentIndexes[$changeIndex - 1]++; 964 | } 965 | } 966 | 967 | return $this->newCollection($result); 968 | } 969 | 970 | /** 971 | * {@inheritDoc} 972 | * 973 | * @return \Cake\Collection\CollectionInterface 974 | * @throws \LogicException 975 | */ 976 | public function transpose(): CollectionInterface 977 | { 978 | $arrayValue = $this->toList(); 979 | $length = count(current($arrayValue)); 980 | $result = []; 981 | foreach ($arrayValue as $row) { 982 | if (count($row) !== $length) { 983 | throw new LogicException('Child arrays do not have even length'); 984 | } 985 | } 986 | 987 | for ($column = 0; $column < $length; $column++) { 988 | $result[] = array_column($arrayValue, $column); 989 | } 990 | 991 | return $this->newCollection($result); 992 | } 993 | 994 | /** 995 | * @inheritDoc 996 | */ 997 | public function count(): int 998 | { 999 | $traversable = $this->optimizeUnwrap(); 1000 | 1001 | if (is_array($traversable)) { 1002 | return count($traversable); 1003 | } 1004 | 1005 | return iterator_count($traversable); 1006 | } 1007 | 1008 | /** 1009 | * @inheritDoc 1010 | */ 1011 | public function countKeys(): int 1012 | { 1013 | return count($this->toArray()); 1014 | } 1015 | 1016 | /** 1017 | * Unwraps this iterator and returns the simplest 1018 | * traversable that can be used for getting the data out 1019 | * 1020 | * @return iterable 1021 | */ 1022 | protected function optimizeUnwrap(): iterable 1023 | { 1024 | /** @var \ArrayObject $iterator */ 1025 | $iterator = $this->unwrap(); 1026 | 1027 | if (get_class($iterator) === ArrayIterator::class) { 1028 | $iterator = $iterator->getArrayCopy(); 1029 | } 1030 | 1031 | return $iterator; 1032 | } 1033 | } 1034 | -------------------------------------------------------------------------------- /CollectionInterface.php: -------------------------------------------------------------------------------- 1 | each(function ($value, $key) { 37 | * echo "Element $key: $value"; 38 | * }); 39 | * ``` 40 | * 41 | * @param callable $callback Callback to run for each element in collection. 42 | * @return $this 43 | */ 44 | public function each(callable $callback); 45 | 46 | /** 47 | * Looks through each value in the collection, and returns another collection with 48 | * all the values that pass a truth test. Only the values for which the callback 49 | * returns true will be present in the resulting collection. 50 | * 51 | * Each time the callback is executed it will receive the value of the element 52 | * in the current iteration, the key of the element and this collection as 53 | * arguments, in that order. 54 | * 55 | * ### Example: 56 | * 57 | * Filtering odd numbers in an array, at the end only the value 2 will 58 | * be present in the resulting collection: 59 | * 60 | * ``` 61 | * $collection = (new Collection([1, 2, 3]))->filter(function ($value, $key) { 62 | * return $value % 2 === 0; 63 | * }); 64 | * ``` 65 | * 66 | * @param callable|null $callback the method that will receive each of the elements and 67 | * returns true whether or not they should be in the resulting collection. 68 | * If left null, a callback that filters out falsey values will be used. 69 | * @return self 70 | */ 71 | public function filter(?callable $callback = null): CollectionInterface; 72 | 73 | /** 74 | * Looks through each value in the collection, and returns another collection with 75 | * all the values that do not pass a truth test. This is the opposite of `filter`. 76 | * 77 | * Each time the callback is executed it will receive the value of the element 78 | * in the current iteration, the key of the element and this collection as 79 | * arguments, in that order. 80 | * 81 | * ### Example: 82 | * 83 | * Filtering even numbers in an array, at the end only values 1 and 3 will 84 | * be present in the resulting collection: 85 | * 86 | * ``` 87 | * $collection = (new Collection([1, 2, 3]))->reject(function ($value, $key) { 88 | * return $value % 2 === 0; 89 | * }); 90 | * ``` 91 | * 92 | * @param callable $callback the method that will receive each of the elements and 93 | * returns true whether or not they should be out of the resulting collection. 94 | * @return self 95 | */ 96 | public function reject(callable $callback): CollectionInterface; 97 | 98 | /** 99 | * Returns true if all values in this collection pass the truth test provided 100 | * in the callback. 101 | * 102 | * Each time the callback is executed it will receive the value of the element 103 | * in the current iteration and the key of the element as arguments, in that 104 | * order. 105 | * 106 | * ### Example: 107 | * 108 | * ``` 109 | * $overTwentyOne = (new Collection([24, 45, 60, 15]))->every(function ($value, $key) { 110 | * return $value > 21; 111 | * }); 112 | * ``` 113 | * 114 | * Empty collections always return true because it is a vacuous truth. 115 | * 116 | * @param callable $callback a callback function 117 | * @return bool true if for all elements in this collection the provided 118 | * callback returns true, false otherwise. 119 | */ 120 | public function every(callable $callback): bool; 121 | 122 | /** 123 | * Returns true if any of the values in this collection pass the truth test 124 | * provided in the callback. 125 | * 126 | * Each time the callback is executed it will receive the value of the element 127 | * in the current iteration and the key of the element as arguments, in that 128 | * order. 129 | * 130 | * ### Example: 131 | * 132 | * ``` 133 | * $hasYoungPeople = (new Collection([24, 45, 15]))->every(function ($value, $key) { 134 | * return $value < 21; 135 | * }); 136 | * ``` 137 | * 138 | * @param callable $callback a callback function 139 | * @return bool true if the provided callback returns true for any element in this 140 | * collection, false otherwise 141 | */ 142 | public function some(callable $callback): bool; 143 | 144 | /** 145 | * Returns true if $value is present in this collection. Comparisons are made 146 | * both by value and type. 147 | * 148 | * @param mixed $value The value to check for 149 | * @return bool true if $value is present in this collection 150 | */ 151 | public function contains($value): bool; 152 | 153 | /** 154 | * Returns another collection after modifying each of the values in this one using 155 | * the provided callable. 156 | * 157 | * Each time the callback is executed it will receive the value of the element 158 | * in the current iteration, the key of the element and this collection as 159 | * arguments, in that order. 160 | * 161 | * ### Example: 162 | * 163 | * Getting a collection of booleans where true indicates if a person is female: 164 | * 165 | * ``` 166 | * $collection = (new Collection($people))->map(function ($person, $key) { 167 | * return $person->gender === 'female'; 168 | * }); 169 | * ``` 170 | * 171 | * @param callable $callback the method that will receive each of the elements and 172 | * returns the new value for the key that is being iterated 173 | * @return self 174 | */ 175 | public function map(callable $callback): CollectionInterface; 176 | 177 | /** 178 | * Folds the values in this collection to a single value, as the result of 179 | * applying the callback function to all elements. $zero is the initial state 180 | * of the reduction, and each successive step of it should be returned 181 | * by the callback function. 182 | * If $zero is omitted the first value of the collection will be used in its place 183 | * and reduction will start from the second item. 184 | * 185 | * @param callable $callback The callback function to be called 186 | * @param mixed $initial The state of reduction 187 | * @return mixed 188 | */ 189 | public function reduce(callable $callback, $initial = null); 190 | 191 | /** 192 | * Returns a new collection containing the column or property value found in each 193 | * of the elements. 194 | * 195 | * The matcher can be a string with a property name to extract or a dot separated 196 | * path of properties that should be followed to get the last one in the path. 197 | * 198 | * If a column or property could not be found for a particular element in the 199 | * collection, that position is filled with null. 200 | * 201 | * ### Example: 202 | * 203 | * Extract the user name for all comments in the array: 204 | * 205 | * ``` 206 | * $items = [ 207 | * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], 208 | * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] 209 | * ]; 210 | * $extracted = (new Collection($items))->extract('comment.user.name'); 211 | * 212 | * // Result will look like this when converted to array 213 | * ['Mark', 'Renan'] 214 | * ``` 215 | * 216 | * It is also possible to extract a flattened collection out of nested properties 217 | * 218 | * ``` 219 | * $items = [ 220 | * ['comment' => ['votes' => [['value' => 1], ['value' => 2], ['value' => 3]]], 221 | * ['comment' => ['votes' => [['value' => 4]] 222 | * ]; 223 | * $extracted = (new Collection($items))->extract('comment.votes.{*}.value'); 224 | * 225 | * // Result will contain 226 | * [1, 2, 3, 4] 227 | * ``` 228 | * 229 | * @param string|callable $path A dot separated path of column to follow 230 | * so that the final one can be returned or a callable that will take care 231 | * of doing that. 232 | * @return self 233 | */ 234 | public function extract($path): CollectionInterface; 235 | 236 | /** 237 | * Returns the top element in this collection after being sorted by a property. 238 | * Check the sortBy method for information on the callback and $sort parameters 239 | * 240 | * ### Examples: 241 | * 242 | * ``` 243 | * // For a collection of employees 244 | * $max = $collection->max('age'); 245 | * $max = $collection->max('user.salary'); 246 | * $max = $collection->max(function ($e) { 247 | * return $e->get('user')->get('salary'); 248 | * }); 249 | * 250 | * // Display employee name 251 | * echo $max->name; 252 | * ``` 253 | * 254 | * @param callable|string $path The column name to use for sorting or callback that returns the value. 255 | * @param int $sort The sort type, one of SORT_STRING 256 | * SORT_NUMERIC or SORT_NATURAL 257 | * @see \Cake\Collection\CollectionInterface::sortBy() 258 | * @return mixed The value of the top element in the collection 259 | */ 260 | public function max($path, int $sort = \SORT_NUMERIC); 261 | 262 | /** 263 | * Returns the bottom element in this collection after being sorted by a property. 264 | * Check the sortBy method for information on the callback and $sort parameters 265 | * 266 | * ### Examples: 267 | * 268 | * ``` 269 | * // For a collection of employees 270 | * $min = $collection->min('age'); 271 | * $min = $collection->min('user.salary'); 272 | * $min = $collection->min(function ($e) { 273 | * return $e->get('user')->get('salary'); 274 | * }); 275 | * 276 | * // Display employee name 277 | * echo $min->name; 278 | * ``` 279 | * 280 | * @param callable|string $path The column name to use for sorting or callback that returns the value. 281 | * @param int $sort The sort type, one of SORT_STRING 282 | * SORT_NUMERIC or SORT_NATURAL 283 | * @see \Cake\Collection\CollectionInterface::sortBy() 284 | * @return mixed The value of the bottom element in the collection 285 | */ 286 | public function min($path, int $sort = \SORT_NUMERIC); 287 | 288 | /** 289 | * Returns the average of all the values extracted with $path 290 | * or of this collection. 291 | * 292 | * ### Example: 293 | * 294 | * ``` 295 | * $items = [ 296 | * ['invoice' => ['total' => 100]], 297 | * ['invoice' => ['total' => 200]] 298 | * ]; 299 | * 300 | * $total = (new Collection($items))->avg('invoice.total'); 301 | * 302 | * // Total: 150 303 | * 304 | * $total = (new Collection([1, 2, 3]))->avg(); 305 | * // Total: 2 306 | * ``` 307 | * 308 | * The average of an empty set or 0 rows is `null`. Collections with `null` 309 | * values are not considered empty. 310 | * 311 | * @param string|callable|null $path The property name to sum or a function 312 | * If no value is passed, an identity function will be used. 313 | * that will return the value of the property to sum. 314 | * @return float|int|null 315 | */ 316 | public function avg($path = null); 317 | 318 | /** 319 | * Returns the median of all the values extracted with $path 320 | * or of this collection. 321 | * 322 | * ### Example: 323 | * 324 | * ``` 325 | * $items = [ 326 | * ['invoice' => ['total' => 400]], 327 | * ['invoice' => ['total' => 500]] 328 | * ['invoice' => ['total' => 100]] 329 | * ['invoice' => ['total' => 333]] 330 | * ['invoice' => ['total' => 200]] 331 | * ]; 332 | * 333 | * $total = (new Collection($items))->median('invoice.total'); 334 | * 335 | * // Total: 333 336 | * 337 | * $total = (new Collection([1, 2, 3, 4]))->median(); 338 | * // Total: 2.5 339 | * ``` 340 | * 341 | * The median of an empty set or 0 rows is `null`. Collections with `null` 342 | * values are not considered empty. 343 | * 344 | * @param string|callable|null $path The property name to sum or a function 345 | * If no value is passed, an identity function will be used. 346 | * that will return the value of the property to sum. 347 | * @return float|int|null 348 | */ 349 | public function median($path = null); 350 | 351 | /** 352 | * Returns a sorted iterator out of the elements in this collection, 353 | * ranked in ascending order by the results of running each value through a 354 | * callback. $callback can also be a string representing the column or property 355 | * name. 356 | * 357 | * The callback will receive as its first argument each of the elements in $items, 358 | * the value returned by the callback will be used as the value for sorting such 359 | * element. Please note that the callback function could be called more than once 360 | * per element. 361 | * 362 | * ### Example: 363 | * 364 | * ``` 365 | * $items = $collection->sortBy(function ($user) { 366 | * return $user->age; 367 | * }); 368 | * 369 | * // alternatively 370 | * $items = $collection->sortBy('age'); 371 | * 372 | * // or use a property path 373 | * $items = $collection->sortBy('department.name'); 374 | * 375 | * // output all user name order by their age in descending order 376 | * foreach ($items as $user) { 377 | * echo $user->name; 378 | * } 379 | * ``` 380 | * 381 | * @param callable|string $path The column name to use for sorting or callback that returns the value. 382 | * @param int $order The sort order, either SORT_DESC or SORT_ASC 383 | * @param int $sort The sort type, one of SORT_STRING 384 | * SORT_NUMERIC or SORT_NATURAL 385 | * @return self 386 | */ 387 | public function sortBy($path, int $order = SORT_DESC, int $sort = \SORT_NUMERIC): CollectionInterface; 388 | 389 | /** 390 | * Splits a collection into sets, grouped by the result of running each value 391 | * through the callback. If $callback is a string instead of a callable, 392 | * groups by the property named by $callback on each of the values. 393 | * 394 | * When $callback is a string it should be a property name to extract or 395 | * a dot separated path of properties that should be followed to get the last 396 | * one in the path. 397 | * 398 | * ### Example: 399 | * 400 | * ``` 401 | * $items = [ 402 | * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], 403 | * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], 404 | * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], 405 | * ]; 406 | * 407 | * $group = (new Collection($items))->groupBy('parent_id'); 408 | * 409 | * // Or 410 | * $group = (new Collection($items))->groupBy(function ($e) { 411 | * return $e['parent_id']; 412 | * }); 413 | * 414 | * // Result will look like this when converted to array 415 | * [ 416 | * 10 => [ 417 | * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], 418 | * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], 419 | * ], 420 | * 11 => [ 421 | * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], 422 | * ] 423 | * ]; 424 | * ``` 425 | * 426 | * @param callable|string $path The column name to use for grouping or callback that returns the value. 427 | * or a function returning the grouping key out of the provided element 428 | * @return self 429 | */ 430 | public function groupBy($path): CollectionInterface; 431 | 432 | /** 433 | * Given a list and a callback function that returns a key for each element 434 | * in the list (or a property name), returns an object with an index of each item. 435 | * Just like groupBy, but for when you know your keys are unique. 436 | * 437 | * When $callback is a string it should be a property name to extract or 438 | * a dot separated path of properties that should be followed to get the last 439 | * one in the path. 440 | * 441 | * ### Example: 442 | * 443 | * ``` 444 | * $items = [ 445 | * ['id' => 1, 'name' => 'foo'], 446 | * ['id' => 2, 'name' => 'bar'], 447 | * ['id' => 3, 'name' => 'baz'], 448 | * ]; 449 | * 450 | * $indexed = (new Collection($items))->indexBy('id'); 451 | * 452 | * // Or 453 | * $indexed = (new Collection($items))->indexBy(function ($e) { 454 | * return $e['id']; 455 | * }); 456 | * 457 | * // Result will look like this when converted to array 458 | * [ 459 | * 1 => ['id' => 1, 'name' => 'foo'], 460 | * 3 => ['id' => 3, 'name' => 'baz'], 461 | * 2 => ['id' => 2, 'name' => 'bar'], 462 | * ]; 463 | * ``` 464 | * 465 | * @param callable|string $path The column name to use for indexing or callback that returns the value. 466 | * or a function returning the indexing key out of the provided element 467 | * @return self 468 | */ 469 | public function indexBy($path): CollectionInterface; 470 | 471 | /** 472 | * Sorts a list into groups and returns a count for the number of elements 473 | * in each group. Similar to groupBy, but instead of returning a list of values, 474 | * returns a count for the number of values in that group. 475 | * 476 | * When $callback is a string it should be a property name to extract or 477 | * a dot separated path of properties that should be followed to get the last 478 | * one in the path. 479 | * 480 | * ### Example: 481 | * 482 | * ``` 483 | * $items = [ 484 | * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], 485 | * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], 486 | * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], 487 | * ]; 488 | * 489 | * $group = (new Collection($items))->countBy('parent_id'); 490 | * 491 | * // Or 492 | * $group = (new Collection($items))->countBy(function ($e) { 493 | * return $e['parent_id']; 494 | * }); 495 | * 496 | * // Result will look like this when converted to array 497 | * [ 498 | * 10 => 2, 499 | * 11 => 1 500 | * ]; 501 | * ``` 502 | * 503 | * @param callable|string $path The column name to use for indexing or callback that returns the value. 504 | * or a function returning the indexing key out of the provided element 505 | * @return self 506 | */ 507 | public function countBy($path): CollectionInterface; 508 | 509 | /** 510 | * Returns the total sum of all the values extracted with $matcher 511 | * or of this collection. 512 | * 513 | * ### Example: 514 | * 515 | * ``` 516 | * $items = [ 517 | * ['invoice' => ['total' => 100]], 518 | * ['invoice' => ['total' => 200]] 519 | * ]; 520 | * 521 | * $total = (new Collection($items))->sumOf('invoice.total'); 522 | * 523 | * // Total: 300 524 | * 525 | * $total = (new Collection([1, 2, 3]))->sumOf(); 526 | * // Total: 6 527 | * ``` 528 | * 529 | * @param string|callable|null $path The property name to sum or a function 530 | * If no value is passed, an identity function will be used. 531 | * that will return the value of the property to sum. 532 | * @return float|int 533 | */ 534 | public function sumOf($path = null); 535 | 536 | /** 537 | * Returns a new collection with the elements placed in a random order, 538 | * this function does not preserve the original keys in the collection. 539 | * 540 | * @return self 541 | */ 542 | public function shuffle(): CollectionInterface; 543 | 544 | /** 545 | * Returns a new collection with maximum $size random elements 546 | * from this collection 547 | * 548 | * @param int $length the maximum number of elements to randomly 549 | * take from this collection 550 | * @return self 551 | */ 552 | public function sample(int $length = 10): CollectionInterface; 553 | 554 | /** 555 | * Returns a new collection with maximum $size elements in the internal 556 | * order this collection was created. If a second parameter is passed, it 557 | * will determine from what position to start taking elements. 558 | * 559 | * @param int $length the maximum number of elements to take from 560 | * this collection 561 | * @param int $offset A positional offset from where to take the elements 562 | * @return self 563 | */ 564 | public function take(int $length = 1, int $offset = 0): CollectionInterface; 565 | 566 | /** 567 | * Returns the last N elements of a collection 568 | * 569 | * ### Example: 570 | * 571 | * ``` 572 | * $items = [1, 2, 3, 4, 5]; 573 | * 574 | * $last = (new Collection($items))->takeLast(3); 575 | * 576 | * // Result will look like this when converted to array 577 | * [3, 4, 5]; 578 | * ``` 579 | * 580 | * @param int $length The number of elements at the end of the collection 581 | * @return self 582 | */ 583 | public function takeLast(int $length): CollectionInterface; 584 | 585 | /** 586 | * Returns a new collection that will skip the specified amount of elements 587 | * at the beginning of the iteration. 588 | * 589 | * @param int $length The number of elements to skip. 590 | * @return self 591 | */ 592 | public function skip(int $length): CollectionInterface; 593 | 594 | /** 595 | * Looks through each value in the list, returning a Collection of all the 596 | * values that contain all of the key-value pairs listed in $conditions. 597 | * 598 | * ### Example: 599 | * 600 | * ``` 601 | * $items = [ 602 | * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], 603 | * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] 604 | * ]; 605 | * 606 | * $extracted = (new Collection($items))->match(['user.name' => 'Renan']); 607 | * 608 | * // Result will look like this when converted to array 609 | * [ 610 | * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] 611 | * ] 612 | * ``` 613 | * 614 | * @param array $conditions a key-value list of conditions where 615 | * the key is a property path as accepted by `Collection::extract, 616 | * and the value the condition against with each element will be matched 617 | * @return self 618 | */ 619 | public function match(array $conditions): CollectionInterface; 620 | 621 | /** 622 | * Returns the first result matching all of the key-value pairs listed in 623 | * conditions. 624 | * 625 | * @param array $conditions a key-value list of conditions where the key is 626 | * a property path as accepted by `Collection::extract`, and the value the 627 | * condition against with each element will be matched 628 | * @see \Cake\Collection\CollectionInterface::match() 629 | * @return mixed 630 | */ 631 | public function firstMatch(array $conditions); 632 | 633 | /** 634 | * Returns the first result in this collection 635 | * 636 | * @return mixed The first value in the collection will be returned. 637 | */ 638 | public function first(); 639 | 640 | /** 641 | * Returns the last result in this collection 642 | * 643 | * @return mixed The last value in the collection will be returned. 644 | */ 645 | public function last(); 646 | 647 | /** 648 | * Returns a new collection as the result of concatenating the list of elements 649 | * in this collection with the passed list of elements 650 | * 651 | * @param iterable $items Items list. 652 | * @return self 653 | */ 654 | public function append($items): CollectionInterface; 655 | 656 | /** 657 | * Append a single item creating a new collection. 658 | * 659 | * @param mixed $item The item to append. 660 | * @param mixed $key The key to append the item with. If null a key will be generated. 661 | * @return self 662 | */ 663 | public function appendItem($item, $key = null): CollectionInterface; 664 | 665 | /** 666 | * Prepend a set of items to a collection creating a new collection 667 | * 668 | * @param mixed $items The items to prepend. 669 | * @return self 670 | */ 671 | public function prepend($items): CollectionInterface; 672 | 673 | /** 674 | * Prepend a single item creating a new collection. 675 | * 676 | * @param mixed $item The item to prepend. 677 | * @param mixed $key The key to prepend the item with. If null a key will be generated. 678 | * @return self 679 | */ 680 | public function prependItem($item, $key = null): CollectionInterface; 681 | 682 | /** 683 | * Returns a new collection where the values extracted based on a value path 684 | * and then indexed by a key path. Optionally this method can produce parent 685 | * groups based on a group property path. 686 | * 687 | * ### Examples: 688 | * 689 | * ``` 690 | * $items = [ 691 | * ['id' => 1, 'name' => 'foo', 'parent' => 'a'], 692 | * ['id' => 2, 'name' => 'bar', 'parent' => 'b'], 693 | * ['id' => 3, 'name' => 'baz', 'parent' => 'a'], 694 | * ]; 695 | * 696 | * $combined = (new Collection($items))->combine('id', 'name'); 697 | * 698 | * // Result will look like this when converted to array 699 | * [ 700 | * 1 => 'foo', 701 | * 2 => 'bar', 702 | * 3 => 'baz', 703 | * ]; 704 | * 705 | * $combined = (new Collection($items))->combine('id', 'name', 'parent'); 706 | * 707 | * // Result will look like this when converted to array 708 | * [ 709 | * 'a' => [1 => 'foo', 3 => 'baz'], 710 | * 'b' => [2 => 'bar'] 711 | * ]; 712 | * ``` 713 | * 714 | * @param callable|string $keyPath the column name path to use for indexing 715 | * or a function returning the indexing key out of the provided element 716 | * @param callable|string $valuePath the column name path to use as the array value 717 | * or a function returning the value out of the provided element 718 | * @param callable|string|null $groupPath the column name path to use as the parent 719 | * grouping key or a function returning the key out of the provided element 720 | * @return self 721 | */ 722 | public function combine($keyPath, $valuePath, $groupPath = null): CollectionInterface; 723 | 724 | /** 725 | * Returns a new collection where the values are nested in a tree-like structure 726 | * based on an id property path and a parent id property path. 727 | * 728 | * @param callable|string $idPath the column name path to use for determining 729 | * whether an element is parent of another 730 | * @param callable|string $parentPath the column name path to use for determining 731 | * whether an element is child of another 732 | * @param string $nestingKey The key name under which children are nested 733 | * @return self 734 | */ 735 | public function nest($idPath, $parentPath, string $nestingKey = 'children'): CollectionInterface; 736 | 737 | /** 738 | * Returns a new collection containing each of the elements found in `$values` as 739 | * a property inside the corresponding elements in this collection. The property 740 | * where the values will be inserted is described by the `$path` parameter. 741 | * 742 | * The $path can be a string with a property name or a dot separated path of 743 | * properties that should be followed to get the last one in the path. 744 | * 745 | * If a column or property could not be found for a particular element in the 746 | * collection as part of the path, the element will be kept unchanged. 747 | * 748 | * ### Example: 749 | * 750 | * Insert ages into a collection containing users: 751 | * 752 | * ``` 753 | * $items = [ 754 | * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], 755 | * ['comment' => ['body' => 'awesome', 'user' => ['name' => 'Renan']] 756 | * ]; 757 | * $ages = [25, 28]; 758 | * $inserted = (new Collection($items))->insert('comment.user.age', $ages); 759 | * 760 | * // Result will look like this when converted to array 761 | * [ 762 | * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark', 'age' => 25]], 763 | * ['comment' => ['body' => 'awesome', 'user' => ['name' => 'Renan', 'age' => 28]] 764 | * ]; 765 | * ``` 766 | * 767 | * @param string $path a dot separated string symbolizing the path to follow 768 | * inside the hierarchy of each value so that the value can be inserted 769 | * @param mixed $values The values to be inserted at the specified path, 770 | * values are matched with the elements in this collection by its positional index. 771 | * @return self 772 | */ 773 | public function insert(string $path, $values): CollectionInterface; 774 | 775 | /** 776 | * Returns an array representation of the results 777 | * 778 | * @param bool $preserveKeys whether to use the keys returned by this 779 | * collection as the array keys. Keep in mind that it is valid for iterators 780 | * to return the same key for different elements, setting this value to false 781 | * can help getting all items if keys are not important in the result. 782 | * @return array 783 | */ 784 | public function toArray(bool $preserveKeys = true): array; 785 | 786 | /** 787 | * Returns an numerically-indexed array representation of the results. 788 | * This is equivalent to calling `toArray(false)` 789 | * 790 | * @return array 791 | */ 792 | public function toList(): array; 793 | 794 | /** 795 | * Returns the data that can be converted to JSON. This returns the same data 796 | * as `toArray()` which contains only unique keys. 797 | * 798 | * Part of JsonSerializable interface. 799 | * 800 | * @return array The data to convert to JSON 801 | */ 802 | public function jsonSerialize(): array; 803 | 804 | /** 805 | * Iterates once all elements in this collection and executes all stacked 806 | * operations of them, finally it returns a new collection with the result. 807 | * This is useful for converting non-rewindable internal iterators into 808 | * a collection that can be rewound and used multiple times. 809 | * 810 | * A common use case is to re-use the same variable for calculating different 811 | * data. In those cases it may be helpful and more performant to first compile 812 | * a collection and then apply more operations to it. 813 | * 814 | * ### Example: 815 | * 816 | * ``` 817 | * $collection->map($mapper)->sortBy('age')->extract('name'); 818 | * $compiled = $collection->compile(); 819 | * $isJohnHere = $compiled->some($johnMatcher); 820 | * $allButJohn = $compiled->filter($johnMatcher); 821 | * ``` 822 | * 823 | * In the above example, had the collection not been compiled before, the 824 | * iterations for `map`, `sortBy` and `extract` would've been executed twice: 825 | * once for getting `$isJohnHere` and once for `$allButJohn` 826 | * 827 | * You can think of this method as a way to create save points for complex 828 | * calculations in a collection. 829 | * 830 | * @param bool $preserveKeys whether to use the keys returned by this 831 | * collection as the array keys. Keep in mind that it is valid for iterators 832 | * to return the same key for different elements, setting this value to false 833 | * can help getting all items if keys are not important in the result. 834 | * @return self 835 | */ 836 | public function compile(bool $preserveKeys = true): CollectionInterface; 837 | 838 | /** 839 | * Returns a new collection where any operations chained after it are guaranteed 840 | * to be run lazily. That is, elements will be yieleded one at a time. 841 | * 842 | * A lazy collection can only be iterated once. A second attempt results in an error. 843 | * 844 | * @return self 845 | */ 846 | public function lazy(): CollectionInterface; 847 | 848 | /** 849 | * Returns a new collection where the operations performed by this collection. 850 | * No matter how many times the new collection is iterated, those operations will 851 | * only be performed once. 852 | * 853 | * This can also be used to make any non-rewindable iterator rewindable. 854 | * 855 | * @return self 856 | */ 857 | public function buffered(): CollectionInterface; 858 | 859 | /** 860 | * Returns a new collection with each of the elements of this collection 861 | * after flattening the tree structure. The tree structure is defined 862 | * by nesting elements under a key with a known name. It is possible 863 | * to specify such name by using the '$nestingKey' parameter. 864 | * 865 | * By default all elements in the tree following a Depth First Search 866 | * will be returned, that is, elements from the top parent to the leaves 867 | * for each branch. 868 | * 869 | * It is possible to return all elements from bottom to top using a Breadth First 870 | * Search approach by passing the '$dir' parameter with 'asc'. That is, it will 871 | * return all elements for the same tree depth first and from bottom to top. 872 | * 873 | * Finally, you can specify to only get a collection with the leaf nodes in the 874 | * tree structure. You do so by passing 'leaves' in the first argument. 875 | * 876 | * The possible values for the first argument are aliases for the following 877 | * constants and it is valid to pass those instead of the alias: 878 | * 879 | * - desc: RecursiveIteratorIterator::SELF_FIRST 880 | * - asc: RecursiveIteratorIterator::CHILD_FIRST 881 | * - leaves: RecursiveIteratorIterator::LEAVES_ONLY 882 | * 883 | * ### Example: 884 | * 885 | * ``` 886 | * $collection = new Collection([ 887 | * ['id' => 1, 'children' => [['id' => 2, 'children' => [['id' => 3]]]]], 888 | * ['id' => 4, 'children' => [['id' => 5]]] 889 | * ]); 890 | * $flattenedIds = $collection->listNested()->extract('id'); // Yields [1, 2, 3, 4, 5] 891 | * ``` 892 | * 893 | * @param string|int $order The order in which to return the elements 894 | * @param string|callable $nestingKey The key name under which children are nested 895 | * or a callable function that will return the children list 896 | * @return self 897 | */ 898 | public function listNested($order = 'desc', $nestingKey = 'children'): CollectionInterface; 899 | 900 | /** 901 | * Creates a new collection that when iterated will stop yielding results if 902 | * the provided condition evaluates to true. 903 | * 904 | * This is handy for dealing with infinite iterators or any generator that 905 | * could start returning invalid elements at a certain point. For example, 906 | * when reading lines from a file stream you may want to stop the iteration 907 | * after a certain value is reached. 908 | * 909 | * ### Example: 910 | * 911 | * Get an array of lines in a CSV file until the timestamp column is less than a date 912 | * 913 | * ``` 914 | * $lines = (new Collection($fileLines))->stopWhen(function ($value, $key) { 915 | * return (new DateTime($value))->format('Y') < 2012; 916 | * }) 917 | * ->toArray(); 918 | * ``` 919 | * 920 | * Get elements until the first unapproved message is found: 921 | * 922 | * ``` 923 | * $comments = (new Collection($comments))->stopWhen(['is_approved' => false]); 924 | * ``` 925 | * 926 | * @param callable|array $condition the method that will receive each of the elements and 927 | * returns true when the iteration should be stopped. 928 | * If an array, it will be interpreted as a key-value list of conditions where 929 | * the key is a property path as accepted by `Collection::extract`, 930 | * and the value the condition against with each element will be matched. 931 | * @return self 932 | */ 933 | public function stopWhen($condition): CollectionInterface; 934 | 935 | /** 936 | * Creates a new collection where the items are the 937 | * concatenation of the lists of items generated by the transformer function 938 | * applied to each item in the original collection. 939 | * 940 | * The transformer function will receive the value and the key for each of the 941 | * items in the collection, in that order, and it must return an array or a 942 | * Traversable object that can be concatenated to the final result. 943 | * 944 | * If no transformer function is passed, an "identity" function will be used. 945 | * This is useful when each of the elements in the source collection are 946 | * lists of items to be appended one after another. 947 | * 948 | * ### Example: 949 | * 950 | * ``` 951 | * $items [[1, 2, 3], [4, 5]]; 952 | * $unfold = (new Collection($items))->unfold(); // Returns [1, 2, 3, 4, 5] 953 | * ``` 954 | * 955 | * Using a transformer 956 | * 957 | * ``` 958 | * $items [1, 2, 3]; 959 | * $allItems = (new Collection($items))->unfold(function ($page) { 960 | * return $service->fetchPage($page)->toArray(); 961 | * }); 962 | * ``` 963 | * 964 | * @param callable|null $callback A callable function that will receive each of 965 | * the items in the collection and should return an array or Traversable object 966 | * @return self 967 | */ 968 | public function unfold(?callable $callback = null): CollectionInterface; 969 | 970 | /** 971 | * Passes this collection through a callable as its first argument. 972 | * This is useful for decorating the full collection with another object. 973 | * 974 | * ### Example: 975 | * 976 | * ``` 977 | * $items = [1, 2, 3]; 978 | * $decorated = (new Collection($items))->through(function ($collection) { 979 | * return new MyCustomCollection($collection); 980 | * }); 981 | * ``` 982 | * 983 | * @param callable $callback A callable function that will receive 984 | * this collection as first argument. 985 | * @return self 986 | */ 987 | public function through(callable $callback): CollectionInterface; 988 | 989 | /** 990 | * Combines the elements of this collection with each of the elements of the 991 | * passed iterables, using their positional index as a reference. 992 | * 993 | * ### Example: 994 | * 995 | * ``` 996 | * $collection = new Collection([1, 2]); 997 | * $collection->zip([3, 4], [5, 6])->toList(); // returns [[1, 3, 5], [2, 4, 6]] 998 | * ``` 999 | * 1000 | * @param iterable ...$items The collections to zip. 1001 | * @return self 1002 | */ 1003 | public function zip(iterable $items): CollectionInterface; 1004 | 1005 | /** 1006 | * Combines the elements of this collection with each of the elements of the 1007 | * passed iterables, using their positional index as a reference. 1008 | * 1009 | * The resulting element will be the return value of the $callable function. 1010 | * 1011 | * ### Example: 1012 | * 1013 | * ``` 1014 | * $collection = new Collection([1, 2]); 1015 | * $zipped = $collection->zipWith([3, 4], [5, 6], function (...$args) { 1016 | * return array_sum($args); 1017 | * }); 1018 | * $zipped->toList(); // returns [9, 12]; [(1 + 3 + 5), (2 + 4 + 6)] 1019 | * ``` 1020 | * 1021 | * @param iterable ...$items The collections to zip. 1022 | * @param callable $callback The function to use for zipping the elements together. 1023 | * @return self 1024 | */ 1025 | public function zipWith(iterable $items, $callback): CollectionInterface; 1026 | 1027 | /** 1028 | * Breaks the collection into smaller arrays of the given size. 1029 | * 1030 | * ### Example: 1031 | * 1032 | * ``` 1033 | * $items [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 1034 | * $chunked = (new Collection($items))->chunk(3)->toList(); 1035 | * // Returns [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11]] 1036 | * ``` 1037 | * 1038 | * @param int $chunkSize The maximum size for each chunk 1039 | * @return self 1040 | */ 1041 | public function chunk(int $chunkSize): CollectionInterface; 1042 | 1043 | /** 1044 | * Breaks the collection into smaller arrays of the given size. 1045 | * 1046 | * ### Example: 1047 | * 1048 | * ``` 1049 | * $items ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6]; 1050 | * $chunked = (new Collection($items))->chunkWithKeys(3)->toList(); 1051 | * // Returns [['a' => 1, 'b' => 2, 'c' => 3], ['d' => 4, 'e' => 5, 'f' => 6]] 1052 | * ``` 1053 | * 1054 | * @param int $chunkSize The maximum size for each chunk 1055 | * @param bool $preserveKeys If the keys of the array should be preserved 1056 | * @return self 1057 | */ 1058 | public function chunkWithKeys(int $chunkSize, bool $preserveKeys = true): CollectionInterface; 1059 | 1060 | /** 1061 | * Returns whether or not there are elements in this collection 1062 | * 1063 | * ### Example: 1064 | * 1065 | * ``` 1066 | * $items [1, 2, 3]; 1067 | * (new Collection($items))->isEmpty(); // false 1068 | * ``` 1069 | * 1070 | * ``` 1071 | * (new Collection([]))->isEmpty(); // true 1072 | * ``` 1073 | * 1074 | * @return bool 1075 | */ 1076 | public function isEmpty(): bool; 1077 | 1078 | /** 1079 | * Returns the closest nested iterator that can be safely traversed without 1080 | * losing any possible transformations. This is used mainly to remove empty 1081 | * IteratorIterator wrappers that can only slowdown the iteration process. 1082 | * 1083 | * @return \Traversable 1084 | */ 1085 | public function unwrap(): Traversable; 1086 | 1087 | /** 1088 | * Transpose rows and columns into columns and rows 1089 | * 1090 | * ### Example: 1091 | * 1092 | * ``` 1093 | * $items = [ 1094 | * ['Products', '2012', '2013', '2014'], 1095 | * ['Product A', '200', '100', '50'], 1096 | * ['Product B', '300', '200', '100'], 1097 | * ['Product C', '400', '300', '200'], 1098 | * ] 1099 | * 1100 | * $transpose = (new Collection($items))->transpose()->toList(); 1101 | * 1102 | * // Returns 1103 | * // [ 1104 | * // ['Products', 'Product A', 'Product B', 'Product C'], 1105 | * // ['2012', '200', '300', '400'], 1106 | * // ['2013', '100', '200', '300'], 1107 | * // ['2014', '50', '100', '200'], 1108 | * // ] 1109 | * ``` 1110 | * 1111 | * @return self 1112 | */ 1113 | public function transpose(): CollectionInterface; 1114 | 1115 | /** 1116 | * Returns the amount of elements in the collection. 1117 | * 1118 | * ## WARNINGS: 1119 | * 1120 | * ### Will change the current position of the iterator: 1121 | * 1122 | * Calling this method at the same time that you are iterating this collections, for example in 1123 | * a foreach, will result in undefined behavior. Avoid doing this. 1124 | * 1125 | * 1126 | * ### Consumes all elements for NoRewindIterator collections: 1127 | * 1128 | * On certain type of collections, calling this method may render unusable afterwards. 1129 | * That is, you may not be able to get elements out of it, or to iterate on it anymore. 1130 | * 1131 | * Specifically any collection wrapping a Generator (a function with a yield statement) 1132 | * or a unbuffered database cursor will not accept any other function calls after calling 1133 | * `count()` on it. 1134 | * 1135 | * Create a new collection with `buffered()` method to overcome this problem. 1136 | * 1137 | * ### Can report more elements than unique keys: 1138 | * 1139 | * Any collection constructed by appending collections together, or by having internal iterators 1140 | * returning duplicate keys, will report a larger amount of elements using this functions than 1141 | * the final amount of elements when converting the collections to a keyed array. This is because 1142 | * duplicate keys will be collapsed into a single one in the final array, whereas this count method 1143 | * is only concerned by the amount of elements after converting it to a plain list. 1144 | * 1145 | * If you need the count of elements after taking the keys in consideration 1146 | * (the count of unique keys), you can call `countKeys()` 1147 | * 1148 | * @return int 1149 | */ 1150 | public function count(): int; 1151 | 1152 | /** 1153 | * Returns the number of unique keys in this iterator. This is the same as the number of 1154 | * elements the collection will contain after calling `toArray()` 1155 | * 1156 | * This method comes with a number of caveats. Please refer to `CollectionInterface::count()` 1157 | * for details. 1158 | * 1159 | * @see \Cake\Collection\CollectionInterface::count() 1160 | * @return int 1161 | */ 1162 | public function countKeys(): int; 1163 | 1164 | /** 1165 | * Create a new collection that is the cartesian product of the current collection 1166 | * 1167 | * In order to create a carteisan product a collection must contain a single dimension 1168 | * of data. 1169 | * 1170 | * ### Example 1171 | * 1172 | * ``` 1173 | * $collection = new Collection([['A', 'B', 'C'], [1, 2, 3]]); 1174 | * $result = $collection->cartesianProduct()->toArray(); 1175 | * $expected = [ 1176 | * ['A', 1], 1177 | * ['A', 2], 1178 | * ['A', 3], 1179 | * ['B', 1], 1180 | * ['B', 2], 1181 | * ['B', 3], 1182 | * ['C', 1], 1183 | * ['C', 2], 1184 | * ['C', 3], 1185 | * ]; 1186 | * ``` 1187 | * 1188 | * @param callable|null $operation A callable that allows you to customize the product result. 1189 | * @param callable|null $filter A filtering callback that must return true for a result to be part 1190 | * of the final results. 1191 | * @return self 1192 | */ 1193 | public function cartesianProduct(?callable $operation = null, ?callable $filter = null): CollectionInterface; 1194 | } 1195 | --------------------------------------------------------------------------------