optimizeUnwrap() as $k => $v) { $c($v, $k); } return $this; } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\FilterIterator */ public function filter(callable $c = null) { if ($c === null) { $c = function ($v) { return (bool)$v; }; } return new FilterIterator($this->unwrap(), $c); } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\FilterIterator */ public function reject(callable $c) { return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($c) { return !$c($key, $value, $items); }); } /** * {@inheritDoc} */ public function every(callable $c) { foreach ($this->optimizeUnwrap() as $key => $value) { if (!$c($value, $key)) { return false; } } return true; } /** * {@inheritDoc} */ public function some(callable $c) { foreach ($this->optimizeUnwrap() as $key => $value) { if ($c($value, $key) === true) { return true; } } return false; } /** * {@inheritDoc} */ public function contains($value) { foreach ($this->optimizeUnwrap() as $v) { if ($value === $v) { return true; } } return false; } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\ReplaceIterator */ public function map(callable $c) { return new ReplaceIterator($this->unwrap(), $c); } /** * {@inheritDoc} */ public function reduce(callable $c, $zero = null) { $isFirst = false; if (func_num_args() < 2) { $isFirst = true; } $result = $zero; foreach ($this->optimizeUnwrap() as $k => $value) { if ($isFirst) { $result = $value; $isFirst = false; continue; } $result = $c($result, $value, $k); } return $result; } /** * {@inheritDoc} */ public function extract($matcher) { $extractor = new ExtractIterator($this->unwrap(), $matcher); if (is_string($matcher) && strpos($matcher, '{*}') !== false) { $extractor = $extractor ->filter(function ($data) { return $data !== null && ($data instanceof Traversable || is_array($data)); }) ->unfold(); } return $extractor; } /** * {@inheritDoc} */ public function max($callback, $type = \SORT_NUMERIC) { return (new SortIterator($this->unwrap(), $callback, \SORT_DESC, $type))->first(); } /** * {@inheritDoc} */ public function min($callback, $type = \SORT_NUMERIC) { return (new SortIterator($this->unwrap(), $callback, \SORT_ASC, $type))->first(); } /** * {@inheritDoc} */ public function avg($matcher = null) { $result = $this; if ($matcher != null) { $result = $result->extract($matcher); } $result = $result ->reduce(function ($acc, $current) { list($count, $sum) = $acc; return [$count + 1, $sum + $current]; }, [0, 0]); if ($result[0] === 0) { return null; } return $result[1] / $result[0]; } /** * {@inheritDoc} */ public function median($matcher = null) { $elements = $this; if ($matcher != null) { $elements = $elements->extract($matcher); } $values = $elements->toList(); sort($values); $count = count($values); if ($count === 0) { return null; } $middle = (int)($count / 2); if ($count % 2) { return $values[$middle]; } return ($values[$middle - 1] + $values[$middle]) / 2; } /** * {@inheritDoc} */ public function sortBy($callback, $dir = \SORT_DESC, $type = \SORT_NUMERIC) { return new SortIterator($this->unwrap(), $callback, $dir, $type); } /** * {@inheritDoc} */ public function groupBy($callback) { $callback = $this->_propertyExtractor($callback); $group = []; foreach ($this->optimizeUnwrap() as $value) { $group[$callback($value)][] = $value; } return $this->newCollection($group); } /** * {@inheritDoc} */ public function indexBy($callback) { $callback = $this->_propertyExtractor($callback); $group = []; foreach ($this->optimizeUnwrap() as $value) { $group[$callback($value)] = $value; } return $this->newCollection($group); } /** * {@inheritDoc} */ public function countBy($callback) { $callback = $this->_propertyExtractor($callback); $mapper = function ($value, $key, $mr) use ($callback) { /** @var \Cake\Collection\Iterator\MapReduce $mr */ $mr->emitIntermediate($value, $callback($value)); }; $reducer = function ($values, $key, $mr) { /** @var \Cake\Collection\Iterator\MapReduce $mr */ $mr->emit(count($values), $key); }; return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)); } /** * {@inheritDoc} */ public function sumOf($matcher = null) { if ($matcher === null) { return array_sum($this->toList()); } $callback = $this->_propertyExtractor($matcher); $sum = 0; foreach ($this->optimizeUnwrap() as $k => $v) { $sum += $callback($v, $k); } return $sum; } /** * {@inheritDoc} */ public function shuffle() { $elements = $this->toArray(); shuffle($elements); return $this->newCollection($elements); } /** * {@inheritDoc} */ public function sample($size = 10) { return $this->newCollection(new LimitIterator($this->shuffle(), 0, $size)); } /** * {@inheritDoc} */ public function take($size = 1, $from = 0) { return $this->newCollection(new LimitIterator($this, $from, $size)); } /** * {@inheritDoc} */ public function skip($howMany) { return $this->newCollection(new LimitIterator($this, $howMany)); } /** * {@inheritDoc} */ public function match(array $conditions) { return $this->filter($this->_createMatcherFilter($conditions)); } /** * {@inheritDoc} */ public function firstMatch(array $conditions) { return $this->match($conditions)->first(); } /** * {@inheritDoc} */ public function first() { $iterator = new LimitIterator($this, 0, 1); foreach ($iterator as $result) { return $result; } } /** * {@inheritDoc} */ public function last() { $iterator = $this->optimizeUnwrap(); if (is_array($iterator)) { return array_pop($iterator); } if ($iterator instanceof Countable) { $count = count($iterator); if ($count === 0) { return null; } $iterator = new LimitIterator($iterator, $count - 1, 1); } $result = null; foreach ($iterator as $result) { // No-op } return $result; } /** * {@inheritDoc} */ public function takeLast($howMany) { if ($howMany < 1) { throw new \InvalidArgumentException("The takeLast method requires a number greater than 0."); } $iterator = $this->optimizeUnwrap(); if (is_array($iterator)) { return $this->newCollection(array_slice($iterator, $howMany * -1)); } if ($iterator instanceof Countable) { $count = count($iterator); if ($count === 0) { return $this->newCollection([]); } $iterator = new LimitIterator($iterator, max(0, $count - $howMany), $howMany); return $this->newCollection($iterator); } $generator = function ($iterator, $howMany) { $result = []; $bucket = 0; $offset = 0; /** * Consider the collection of elements [1, 2, 3, 4, 5, 6, 7, 8, 9], in order * to get the last 4 elements, we can keep a buffer of 4 elements and * fill it circularly using modulo logic, we use the $bucket variable * to track the position to fill next in the buffer. This how the buffer * looks like after 4 iterations: * * 0) 1 2 3 4 -- $bucket now goes back to 0, we have filled 4 elementes * 1) 5 2 3 4 -- 5th iteration * 2) 5 6 3 4 -- 6th iteration * 3) 5 6 7 4 -- 7th iteration * 4) 5 6 7 8 -- 8th iteration * 5) 9 6 7 8 * * We can see that at the end of the iterations, the buffer contains all * the last four elements, just in the wrong order. How do we keep the * original order? Well, it turns out that the number of iteration also * give us a clue on what's going on, Let's add a marker for it now: * * 0) 1 2 3 4 * ^ -- The 0) above now becomes the $offset variable * 1) 5 2 3 4 * ^ -- $offset = 1 * 2) 5 6 3 4 * ^ -- $offset = 2 * 3) 5 6 7 4 * ^ -- $offset = 3 * 4) 5 6 7 8 * ^ -- We use module logic for $offset too * and as you can see each time $offset is 0, then the buffer * is sorted exactly as we need. * 5) 9 6 7 8 * ^ -- $offset = 1 * * The $offset variable is a marker for splitting the buffer in two, * elements to the right for the marker are the head of the final result, * whereas the elements at the left are the tail. For example consider step 5) * which has an offset of 1: * * - $head = elements to the right = [6, 7, 8] * - $tail = elements to the left = [9] * - $result = $head + $tail = [6, 7, 8, 9] * * The logic above applies to collections of any size. */ foreach ($iterator as $k => $item) { $result[$bucket] = [$k, $item]; $bucket = (++$bucket) % $howMany; $offset++; } $offset = $offset % $howMany; $head = array_slice($result, $offset); $tail = array_slice($result, 0, $offset); foreach ($head as $v) { yield $v[0] => $v[1]; } foreach ($tail as $v) { yield $v[0] => $v[1]; } }; return $this->newCollection($generator($iterator, $howMany)); } /** * {@inheritDoc} */ public function append($items) { $list = new AppendIterator(); $list->append($this->unwrap()); $list->append($this->newCollection($items)->unwrap()); return $this->newCollection($list); } /** * {@inheritDoc} */ public function appendItem($item, $key = null) { if ($key !== null) { $data = [$key => $item]; } else { $data = [$item]; } return $this->append($data); } /** * {@inheritDoc} */ public function prepend($items) { return $this->newCollection($items)->append($this); } /** * {@inheritDoc} */ public function prependItem($item, $key = null) { if ($key !== null) { $data = [$key => $item]; } else { $data = [$item]; } return $this->prepend($data); } /** * {@inheritDoc} */ public function combine($keyPath, $valuePath, $groupPath = null) { $options = [ 'keyPath' => $this->_propertyExtractor($keyPath), 'valuePath' => $this->_propertyExtractor($valuePath), 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null, ]; $mapper = function ($value, $key, $mapReduce) use ($options) { /** @var \Cake\Collection\Iterator\MapReduce $mapReduce */ $rowKey = $options['keyPath']; $rowVal = $options['valuePath']; if (!$options['groupPath']) { $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key)); return null; } $key = $options['groupPath']($value, $key); $mapReduce->emitIntermediate( [$rowKey($value, $key) => $rowVal($value, $key)], $key ); }; $reducer = function ($values, $key, $mapReduce) { $result = []; foreach ($values as $value) { $result += $value; } /** @var \Cake\Collection\Iterator\MapReduce $mapReduce */ $mapReduce->emit($result, $key); }; return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)); } /** * {@inheritDoc} */ public function nest($idPath, $parentPath, $nestingKey = 'children') { $parents = []; $idPath = $this->_propertyExtractor($idPath); $parentPath = $this->_propertyExtractor($parentPath); $isObject = true; $mapper = function ($row, $key, $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey) { $row[$nestingKey] = []; $id = $idPath($row, $key); $parentId = $parentPath($row, $key); $parents[$id] =& $row; /** @var \Cake\Collection\Iterator\MapReduce $mapReduce */ $mapReduce->emitIntermediate($id, $parentId); }; $reducer = function ($values, $key, $mapReduce) use (&$parents, &$isObject, $nestingKey) { static $foundOutType = false; if (!$foundOutType) { $isObject = is_object(current($parents)); $foundOutType = true; } if (empty($key) || !isset($parents[$key])) { foreach ($values as $id) { $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1); /** @var \Cake\Collection\Iterator\MapReduce $mapReduce */ $mapReduce->emit($parents[$id]); } return null; } $children = []; foreach ($values as $id) { $children[] =& $parents[$id]; } $parents[$key][$nestingKey] = $children; }; return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer)) ->map(function ($value) use (&$isObject) { /** @var \ArrayIterator $value */ return $isObject ? $value : $value->getArrayCopy(); }); } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\InsertIterator */ public function insert($path, $values) { return new InsertIterator($this->unwrap(), $path, $values); } /** * {@inheritDoc} */ public function toArray($preserveKeys = true) { $iterator = $this->unwrap(); if ($iterator instanceof ArrayIterator) { $items = $iterator->getArrayCopy(); return $preserveKeys ? $items : array_values($items); } // RecursiveIteratorIterator can return duplicate key values causing // data loss when converted into an array if ($preserveKeys && get_class($iterator) === 'RecursiveIteratorIterator') { $preserveKeys = false; } return iterator_to_array($this, $preserveKeys); } /** * {@inheritDoc} */ public function toList() { return $this->toArray(false); } /** * {@inheritDoc} */ public function jsonSerialize() { return $this->toArray(); } /** * {@inheritDoc} */ public function compile($preserveKeys = true) { return $this->newCollection($this->toArray($preserveKeys)); } /** * {@inheritDoc} */ public function lazy() { $generator = function () { foreach ($this->unwrap() as $k => $v) { yield $k => $v; } }; return $this->newCollection($generator()); } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\BufferedIterator */ public function buffered() { return new BufferedIterator($this->unwrap()); } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\TreeIterator */ public function listNested($dir = 'desc', $nestingKey = 'children') { $dir = strtolower($dir); $modes = [ 'desc' => TreeIterator::SELF_FIRST, 'asc' => TreeIterator::CHILD_FIRST, 'leaves' => TreeIterator::LEAVES_ONLY, ]; return new TreeIterator( new NestIterator($this, $nestingKey), isset($modes[$dir]) ? $modes[$dir] : $dir ); } /** * {@inheritDoc} * * @return \Cake\Collection\Iterator\StoppableIterator */ public function stopWhen($condition) { if (!is_callable($condition)) { $condition = $this->_createMatcherFilter($condition); } return new StoppableIterator($this->unwrap(), $condition); } /** * {@inheritDoc} */ public function unfold(callable $transformer = null) { if ($transformer === null) { $transformer = function ($item) { return $item; }; } return $this->newCollection( new RecursiveIteratorIterator( new UnfoldIterator($this->unwrap(), $transformer), RecursiveIteratorIterator::LEAVES_ONLY ) ); } /** * {@inheritDoc} */ public function through(callable $handler) { $result = $handler($this); return $result instanceof CollectionInterface ? $result : $this->newCollection($result); } /** * {@inheritDoc} */ public function zip($items) { return new ZipIterator(array_merge([$this->unwrap()], func_get_args())); } /** * {@inheritDoc} */ public function zipWith($items, $callable) { if (func_num_args() > 2) { $items = func_get_args(); $callable = array_pop($items); } else { $items = [$items]; } return new ZipIterator(array_merge([$this->unwrap()], $items), $callable); } /** * {@inheritDoc} */ public function chunk($chunkSize) { return $this->map(function ($v, $k, $iterator) use ($chunkSize) { $values = [$v]; for ($i = 1; $i < $chunkSize; $i++) { $iterator->next(); if (!$iterator->valid()) { break; } $values[] = $iterator->current(); } return $values; }); } /** * {@inheritDoc} */ public function chunkWithKeys($chunkSize, $preserveKeys = true) { return $this->map(function ($v, $k, $iterator) use ($chunkSize, $preserveKeys) { $key = 0; if ($preserveKeys) { $key = $k; } $values = [$key => $v]; for ($i = 1; $i < $chunkSize; $i++) { $iterator->next(); if (!$iterator->valid()) { break; } if ($preserveKeys) { $values[$iterator->key()] = $iterator->current(); } else { $values[] = $iterator->current(); } } return $values; }); } /** * {@inheritDoc} */ public function isEmpty() { foreach ($this as $el) { return false; } return true; } /** * {@inheritDoc} */ public function unwrap() { $iterator = $this; while (get_class($iterator) === 'Cake\Collection\Collection') { $iterator = $iterator->getInnerIterator(); } if ($iterator !== $this && $iterator instanceof CollectionInterface) { $iterator = $iterator->unwrap(); } return $iterator; } /** * Backwards compatible wrapper for unwrap() * * @return \Traversable * @deprecated 3.0.10 Will be removed in 4.0.0 */ // @codingStandardsIgnoreLine public function _unwrap() { deprecationWarning('CollectionTrait::_unwrap() is deprecated. Use CollectionTrait::unwrap() instead.'); return $this->unwrap(); } /** * @param callable|null $operation Operation * @param callable|null $filter Filter * @return \Cake\Collection\CollectionInterface * @throws \LogicException */ public function cartesianProduct(callable $operation = null, callable $filter = null) { if ($this->isEmpty()) { return $this->newCollection([]); } $collectionArrays = []; $collectionArraysKeys = []; $collectionArraysCounts = []; foreach ($this->toList() as $value) { $valueCount = count($value); if ($valueCount !== count($value, COUNT_RECURSIVE)) { throw new LogicException('Cannot find the cartesian product of a multidimensional array'); } $collectionArraysKeys[] = array_keys($value); $collectionArraysCounts[] = $valueCount; $collectionArrays[] = $value; } $result = []; $lastIndex = count($collectionArrays) - 1; // holds the indexes of the arrays that generate the current combination $currentIndexes = array_fill(0, $lastIndex + 1, 0); $changeIndex = $lastIndex; while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) { $currentCombination = array_map(function ($value, $keys, $index) { return $value[$keys[$index]]; }, $collectionArrays, $collectionArraysKeys, $currentIndexes); if ($filter === null || $filter($currentCombination)) { $result[] = ($operation === null) ? $currentCombination : $operation($currentCombination); } $currentIndexes[$lastIndex]++; for ($changeIndex = $lastIndex; $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0; $changeIndex--) { $currentIndexes[$changeIndex] = 0; $currentIndexes[$changeIndex - 1]++; } } return $this->newCollection($result); } /** * {@inheritDoc} * * @return \Cake\Collection\CollectionInterface * @throws \LogicException */ public function transpose() { $arrayValue = $this->toList(); $length = count(current($arrayValue)); $result = []; foreach ($arrayValue as $column => $row) { if (count($row) != $length) { throw new LogicException('Child arrays do not have even length'); } } for ($column = 0; $column < $length; $column++) { $result[] = array_column($arrayValue, $column); } return $this->newCollection($result); } /** * {@inheritDoc} * * @return int */ public function count() { $traversable = $this->optimizeUnwrap(); if (is_array($traversable)) { return count($traversable); } return iterator_count($traversable); } /** * {@inheritDoc} * * @return int */ public function countKeys() { return count($this->toArray()); } /** * Unwraps this iterator and returns the simplest * traversable that can be used for getting the data out * * @return \Traversable|array */ protected function optimizeUnwrap() { $iterator = $this->unwrap(); if (get_class($iterator) === ArrayIterator::class) { $iterator = $iterator->getArrayCopy(); } return $iterator; } }