CollectionTrait.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since CakePHP(tm) v 3.0.0
  13. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  14. */
  15. namespace Cake\Collection;
  16. use AppendIterator;
  17. use Cake\Collection\Collection;
  18. use Cake\Collection\Iterator\ExtractIterator;
  19. use Cake\Collection\Iterator\FilterIterator;
  20. use Cake\Collection\Iterator\MapReduce;
  21. use Cake\Collection\Iterator\ReplaceIterator;
  22. use Cake\Collection\Iterator\SortIterator;
  23. use LimitIterator;
  24. /**
  25. * Offers a handful of method to manipulate iterators
  26. */
  27. trait CollectionTrait {
  28. use ExtractTrait;
  29. /**
  30. * Executes the passed callable for each of the elements in this collection
  31. * and passes both the value and key for them on each step.
  32. * Returns the same collection for chaining.
  33. *
  34. * ###Example:
  35. *
  36. * {{{
  37. * $collection = (new Collection($items))->each(function($value, $key) {
  38. * echo "Element $key: $value";
  39. * });
  40. * }}}
  41. *
  42. * @param callable $c callable function that will receive each of the elements
  43. * in this collection
  44. * @return \Cake\Collection\Collection
  45. */
  46. public function each(callable $c) {
  47. foreach ($this as $k => $v) {
  48. $c($v, $k);
  49. }
  50. return $this;
  51. }
  52. /**
  53. * Looks through each value in the collection, and returns another collection with
  54. * all the values that pass a truth test. Only the values for which the callback
  55. * returns true will be present in the resulting collection.
  56. *
  57. * Each time the callback is executed it will receive the value of the element
  58. * in the current iteration, the key of the element and this collection as
  59. * arguments, in that order.
  60. *
  61. * ##Example:
  62. *
  63. * Filtering odd numbers in an array, at the end only the value 2 will
  64. * be present in the resulting collection:
  65. *
  66. * {{{
  67. * $collection = (new Collection([1, 2, 3]))->filter(function($value, $key) {
  68. * return $value % 2 === 0;
  69. * });
  70. * }}}
  71. *
  72. * @param callable $c the method that will receive each of the elements and
  73. * returns true whether or not they should be in the resulting collection.
  74. * @return \Cake\Collection\Iterator\FilterIterator;
  75. */
  76. public function filter(callable $c) {
  77. return new FilterIterator($this, $c);
  78. }
  79. /**
  80. * Looks through each value in the collection, and returns another collection with
  81. * all the values that do not pass a truth test. This is the opposite of `filter`.
  82. *
  83. * Each time the callback is executed it will receive the value of the element
  84. * in the current iteration, the key of the element and this collection as
  85. * arguments, in that order.
  86. *
  87. * ##Example:
  88. *
  89. * Filtering even numbers in an array, at the end only values 1 and 3 will
  90. * be present in the resulting collection:
  91. *
  92. * {{{
  93. * $collection = (new Collection([1, 2, 3]))->reject(function($value, $key) {
  94. * return $value % 2 === 0;
  95. * });
  96. * }}}
  97. *
  98. * @param callable $c the method that will receive each of the elements and
  99. * returns true whether or not they should be out of the resulting collection.
  100. * @return \Cake\Collection\Iterator\FilterIterator;
  101. */
  102. public function reject(callable $c) {
  103. return new FilterIterator($this, function ($key, $value, $items) use ($c) {
  104. return !$c($key, $value, $items);
  105. });
  106. }
  107. /**
  108. * Returns true if all values in this collection pass the truth test provided
  109. * in the callback.
  110. *
  111. * Each time the callback is executed it will receive the value of the element
  112. * in the current iteration and the key of the element as arguments, in that
  113. * order.
  114. *
  115. * ###Example:
  116. *
  117. * {{{
  118. * $overTwentyOne = (new Collection([24, 45, 60, 15]))->every(function($value, $key) {
  119. * return $value > 21;
  120. * });
  121. * }}}
  122. *
  123. * @param callable $c a callback function
  124. * @return boolean true if for all elements in this collection the provided
  125. * callback returns true, false otherwise
  126. */
  127. public function every(callable $c) {
  128. foreach ($this as $key => $value) {
  129. if (!$c($value, $key)) {
  130. return false;
  131. }
  132. }
  133. return true;
  134. }
  135. /**
  136. * Returns true if any of the values in this collection pass the truth test
  137. * provided in the callback.
  138. *
  139. * Each time the callback is executed it will receive the value of the element
  140. * in the current iteration and the key of the element as arguments, in that
  141. * order.
  142. *
  143. * ###Example:
  144. *
  145. * {{{
  146. * $hasYoungPeople = (new Collection([24, 45, 15]))->every(function($value, $key) {
  147. * return $value < 21;
  148. * });
  149. * }}}
  150. *
  151. * @param callable $c a callback function
  152. * @return boolean true if for all elements in this collection the provided
  153. * callback returns true, false otherwise
  154. */
  155. public function some(callable $c) {
  156. foreach ($this as $key => $value) {
  157. if ($c($value, $key) === true) {
  158. return true;
  159. }
  160. }
  161. return false;
  162. }
  163. /**
  164. * Returns true if $value is present in this collection. Comparisons are made
  165. * both by value and type.
  166. *
  167. * @param mixed $value the value to check for
  168. * @return boolean true if $value is present in this collection
  169. */
  170. public function contains($value) {
  171. foreach ($this as $v) {
  172. if ($value === $v) {
  173. return true;
  174. }
  175. }
  176. return false;
  177. }
  178. /**
  179. * Returns another collection after modifying each of the values in this one using
  180. * the provided callable.
  181. *
  182. * Each time the callback is executed it will receive the value of the element
  183. * in the current iteration, the key of the element and this collection as
  184. * arguments, in that order.
  185. *
  186. * ##Example:
  187. *
  188. * Getting a collection of booleans where true indicates if a person is female:
  189. *
  190. * {{{
  191. * $collection = (new Collection($people))->map(function($person, $key) {
  192. * return $person->gender === 'female';
  193. * });
  194. * }}}
  195. *
  196. * @param callable $c the method that will receive each of the elements and
  197. * returns the new value for the key that is being iterated
  198. * @return \Cake\Collection\Iterator\ReplaceIterator
  199. */
  200. public function map(callable $c) {
  201. return new ReplaceIterator($this, $c);
  202. }
  203. /**
  204. * Folds the values in this collection to a single value, as the result of
  205. * applying the callback function to all elements. $zero is the initial state
  206. * of the reduction, and each successive step should of it should be returned
  207. * by the callback function.
  208. *
  209. * The callback function is
  210. *
  211. * @return void
  212. */
  213. public function reduce(callable $c, $zero) {
  214. $result = $zero;
  215. foreach ($this as $k => $value) {
  216. $result = $c($result, $value, $k);
  217. }
  218. return $result;
  219. }
  220. /**
  221. * Returns a new collection containing the column or property value found in each
  222. * of the elements, as requested in the $matcher param.
  223. *
  224. * The matcher can be a string with a property name to extract or a dot separated
  225. * path of properties that should be followed to get the last one in the path.
  226. *
  227. * If a column or property could not be found for a particular element in the
  228. * collection, that position is filled with null.
  229. *
  230. * ### Example:
  231. *
  232. * Extract the user name for all comments in the array:
  233. *
  234. * {{{
  235. * $items = [
  236. * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']],
  237. * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']]
  238. * ];
  239. * $extracted = (new Collection($items))->extract('comment.user.name');
  240. *
  241. * //Result will look like this when converted to array
  242. * ['Mark', 'Renan']
  243. * }}}
  244. *
  245. * @param string $path a dot separated string symbolizing the path to follow
  246. * inside the hierarchy of each value so that the column can be extracted.
  247. * @return \Cake\Collection\Iterator\ExtractIterator
  248. */
  249. public function extract($matcher) {
  250. return new ExtractIterator($this, $matcher);
  251. }
  252. /**
  253. * Returns the top element in this collection after being sorted by a property.
  254. * Check the sortBy method for information on the callback and $type parameters
  255. *
  256. * ###Examples:
  257. *
  258. * {{{
  259. * //For a collection of employees
  260. * $max = $collection->max('age');
  261. * $max = $collection->max('user.salary');
  262. * $max = $collection->max(function($e) {
  263. * return $e->get('user')->get('salary');
  264. * });
  265. *
  266. * //Display employee name
  267. * echo $max->name;
  268. * }}}
  269. *
  270. * @param callable|string the callback or column name to use for sorting
  271. * @param integer $type the type of comparison to perform, either SORT_STRING
  272. * SORT_NUMERIC or SORT_NATURAL
  273. * @see \Cake\Collection\Collection::sortBy()
  274. */
  275. public function max($callback, $type = SORT_NUMERIC) {
  276. $sorted = new SortIterator($this, $callback, SORT_DESC, $type);
  277. return $sorted->top();
  278. }
  279. /**
  280. * Returns the bottom element in this collection after being sorted by a property.
  281. * Check the sortBy method for information on the callback and $type parameters
  282. *
  283. * ###Examples:
  284. *
  285. * {{{
  286. * //For a collection of employees
  287. * $min = $collection->min('age');
  288. * $min = $collection->min('user.salary');
  289. * $min = $collection->min(function($e) {
  290. * return $e->get('user')->get('salary');
  291. * });
  292. *
  293. * //Display employee name
  294. * echo $min->name;
  295. * }}}
  296. *
  297. *
  298. * @param callable|string the callback or column name to use for sorting
  299. * @param integer $type the type of comparison to perform, either SORT_STRING
  300. * SORT_NUMERIC or SORT_NATURAL
  301. * @see \Cake\Collection\Collection::sortBy()
  302. */
  303. public function min($callback, $type = SORT_NUMERIC) {
  304. $sorted = new SortIterator($this, $callback, SORT_ASC, $type);
  305. return $sorted->top();
  306. }
  307. /**
  308. * Returns a sorted iterator out of the elements in this colletion,
  309. * ranked in ascending order by the results of running each value through a
  310. * callback. $callback can also be a string representing the column or property
  311. * name.
  312. *
  313. * The callback will receive as its first argument each of the elements in $items,
  314. * the value returned by the callback will be used as the value for sorting such
  315. * element. Please note that the callback function could be called more than once
  316. * per element.
  317. *
  318. * ###Example:
  319. *
  320. * {{{
  321. * $items = $collection->sortBy(function($user) {
  322. * return $user->age;
  323. * });
  324. *
  325. * //alternatively
  326. * $items = $collection->sortBy('age');
  327. *
  328. * //or use a property path
  329. * $items = $collection->sortBy('department.name');
  330. *
  331. * // output all user name order by their age in descending order
  332. * foreach ($items as $user) {
  333. * echo $user->name;
  334. * }
  335. * }}}
  336. *
  337. * @param callable|string the callback or column name to use for sorting
  338. * @param integer $dir either SORT_DESC or SORT_ASC
  339. * @param integer $type the type of comparison to perform, either SORT_STRING
  340. * SORT_NUMERIC or SORT_NATURAL
  341. * @return \Cake\Collection\Collection
  342. */
  343. public function sortBy($callback, $dir = SORT_DESC, $type = SORT_NUMERIC) {
  344. return new Collection(new SortIterator($this, $callback, $dir, $type));
  345. }
  346. /**
  347. * Splits a collection into sets, grouped by the result of running each value
  348. * through the callback. If $callback is is a string instead of a callable,
  349. * groups by the property named by $callback on each of the values.
  350. *
  351. * When $callback is a string it should be a property name to extract or
  352. * a dot separated path of properties that should be followed to get the last
  353. * one in the path.
  354. *
  355. * ###Example:
  356. *
  357. * {{{
  358. * $items = [
  359. * ['id' => 1, 'name' => 'foo', 'parent_id' => 10],
  360. * ['id' => 2, 'name' => 'bar', 'parent_id' => 11],
  361. * ['id' => 3, 'name' => 'baz', 'parent_id' => 10],
  362. * ];
  363. *
  364. * $group = (new Collection($items))->groupBy('parent_id');
  365. *
  366. * //Or
  367. * $group = (new Collection($items))->groupBy(function($e) {
  368. * return $e['parent_id'];
  369. * });
  370. *
  371. * //Result will look like this when converted to array
  372. * [
  373. * 10 => [
  374. * ['id' => 1, 'name' => 'foo', 'parent_id' => 10],
  375. * ['id' => 3, 'name' => 'baz', 'parent_id' => 10],
  376. * ],
  377. * 11 => [
  378. * ['id' => 2, 'name' => 'bar', 'parent_id' => 11],
  379. * ]
  380. * ];
  381. * }}}
  382. *
  383. * @param callable|string the callback or column name to use for grouping
  384. * or a function returning the grouping key out of the provided element
  385. * @return \Cake\Collection\Collection
  386. */
  387. public function groupBy($callback) {
  388. $callback = $this->_propertyExtractor($callback);
  389. $group = [];
  390. foreach ($this as $value) {
  391. $group[$callback($value)][] = $value;
  392. }
  393. return new Collection($group);
  394. }
  395. /**
  396. * Given a list and a callback function that returns a key for each element
  397. * in the list (or a property name), returns an object with an index of each item.
  398. * Just like groupBy, but for when you know your keys are unique.
  399. *
  400. * When $callback is a string it should be a property name to extract or
  401. * a dot separated path of properties that should be followed to get the last
  402. * one in the path.
  403. *
  404. * ###Example:
  405. *
  406. * {{{
  407. * $items = [
  408. * ['id' => 1, 'name' => 'foo'],
  409. * ['id' => 2, 'name' => 'bar'],
  410. * ['id' => 3, 'name' => 'baz'],
  411. * ];
  412. *
  413. * $indexed = (new Collection($items))->indexBy('id');
  414. *
  415. * //Or
  416. * $indexed = (new Collection($items))->indexBy(function($e) {
  417. * return $e['id'];
  418. * });
  419. *
  420. * //Result will look like this when converted to array
  421. * [
  422. * 1 => ['id' => 1, 'name' => 'foo'],
  423. * 3 => ['id' => 3, 'name' => 'baz'],
  424. * 2 => ['id' => 2, 'name' => 'bar'],
  425. * ];
  426. * }}}
  427. *
  428. * @param callable|string the callback or column name to use for indexing
  429. * or a function returning the indexing key out of the provided element
  430. * @return \Cake\Collection\Collection
  431. */
  432. public function indexBy($callback) {
  433. $callback = $this->_propertyExtractor($callback);
  434. $group = [];
  435. foreach ($this as $value) {
  436. $group[$callback($value)] = $value;
  437. }
  438. return new Collection($group);
  439. }
  440. /**
  441. * Sorts a list into groups and returns a count for the number of elements
  442. * in each group. Similar to groupBy, but instead of returning a list of values,
  443. * returns a count for the number of values in that group.
  444. *
  445. * When $callback is a string it should be a property name to extract or
  446. * a dot separated path of properties that should be followed to get the last
  447. * one in the path.
  448. *
  449. * ###Example:
  450. *
  451. * {{{
  452. * $items = [
  453. * ['id' => 1, 'name' => 'foo', 'parent_id' => 10],
  454. * ['id' => 2, 'name' => 'bar', 'parent_id' => 11],
  455. * ['id' => 3, 'name' => 'baz', 'parent_id' => 10],
  456. * ];
  457. *
  458. * $group = (new Collection($items))->countBy('parent_id');
  459. *
  460. * //Or
  461. * $group = (new Collection($items))->countBy(function($e) {
  462. * return $e['parent_id'];
  463. * });
  464. *
  465. * //Result will look like this when converted to array
  466. * [
  467. * 10 => 2,
  468. * 11 => 1
  469. * ];
  470. * }}}
  471. *
  472. * @param callable|string the callback or column name to use for indexing
  473. * or a function returning the indexing key out of the provided element
  474. * @return \Cake\Collection\Collection
  475. */
  476. public function countBy($callback) {
  477. $callback = $this->_propertyExtractor($callback);
  478. $mapper = function($value, $key, $mr) use ($callback) {
  479. $mr->emitIntermediate($value, $callback($value));
  480. };
  481. $reducer = function ($values, $key, $mr) {
  482. $mr->emit(count($values), $key);
  483. };
  484. return new Collection(new MapReduce($this, $mapper, $reducer));
  485. }
  486. /**
  487. * Returns a new collection with the elements placed in a random order,
  488. * this function does not preserve the original keys in the collection.
  489. *
  490. * @return \Cake\Collection\Collection
  491. */
  492. public function shuffle() {
  493. $elements = iterator_to_array($this);
  494. shuffle($elements);
  495. return new Collection($elements);
  496. }
  497. /**
  498. * Returns a new collection with maximum $size random elements
  499. * from this collection
  500. *
  501. * @param integer $size the maximum number of elements to randomly
  502. * take from this collection
  503. * @return \Cake\Collection\Collection
  504. */
  505. public function sample($size = 10) {
  506. return new Collection(new LimitIterator($this->shuffle(), 0, $size));
  507. }
  508. /**
  509. * Returns a new collection with maximum $size elements in the internal
  510. * order this collection was created. If a second parameter is passed, it
  511. * will determine from what position to start taking elements.
  512. *
  513. * @param integer $size the maximum number of elements to take from
  514. * this collection
  515. * @param integer $from A positional offset from where to take the elements
  516. * @return \Cake\Collection\Collection
  517. */
  518. public function take($size = 1, $from = 0) {
  519. return new Collection(new LimitIterator($this, $from, $size));
  520. }
  521. /**
  522. * Looks through each value in the list, returning a Collection of all the
  523. * values that contain all of the key-value pairs listed in $conditions.
  524. *
  525. * ###Example:
  526. *
  527. * {{{
  528. * $items = [
  529. * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']],
  530. * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']]
  531. * ];
  532. *
  533. * $extracted = (new Collection($items))->match(['user.name' => 'Renan']);
  534. *
  535. * //Result will look like this when converted to array
  536. * [
  537. * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']]
  538. * ]
  539. * }}}
  540. *
  541. * @param array $conditions a key-value list of conditions where
  542. * the key is a property path as accepted by `Collection::extract,
  543. * and the value the condition against with each element will be matched
  544. * @return \Cake\Collection\Collection
  545. */
  546. public function match(array $conditions) {
  547. $matchers = [];
  548. foreach ($conditions as $property => $value) {
  549. $extractor = $this->_propertyExtractor($property);
  550. $matchers[] = function($v) use ($extractor, $value) {
  551. return $extractor($v) == $value;
  552. };
  553. }
  554. $filter = function($value) use ($matchers) {
  555. $valid = true;
  556. foreach ($matchers as $match) {
  557. $valid = $valid && $match($value);
  558. }
  559. return $valid;
  560. };
  561. return $this->filter($filter);
  562. }
  563. /**
  564. * Returns the first result matching all of the key-value pairs listed in
  565. * conditions.
  566. *
  567. * @param array $conditions a key-value list of conditions where the key is
  568. * a property path as accepted by `Collection::extract`, and the value the
  569. * condition against with each element will be matched
  570. * @see \Cake\Collection\Collection::match()
  571. * @return mixed
  572. */
  573. public function firstMatch(array $conditions) {
  574. return $this->match($conditions)->first();
  575. }
  576. /**
  577. * Returns the first result in this collection
  578. *
  579. * @return mixed The first value in the collection will be returned.
  580. */
  581. public function first() {
  582. foreach ($this->take(1) as $result) {
  583. return $result;
  584. }
  585. }
  586. /**
  587. * Returns a new collection as the result of concatenating the list of elements
  588. * in this collection with the passed list of elements
  589. *
  590. * @param array|\Traversable
  591. * @return \Cake\Collection\Collection
  592. */
  593. public function append($items) {
  594. $list = new AppendIterator;
  595. $list->append($this);
  596. $list->append(new Collection($items));
  597. return new Collection($list);
  598. }
  599. /**
  600. * Returns an array representation of the results
  601. *
  602. * @param boolean $preserveKeys whether to use the keys returned by this
  603. * collection as the array keys. Keep in mind that it is valid for iterators
  604. * to return the same key for different elements, setting this value to false
  605. * can help getting all items if keys are not important in the result.
  606. * @return array
  607. */
  608. public function toArray($preserveKeys = true) {
  609. return iterator_to_array($this, $preserveKeys);
  610. }
  611. /**
  612. * Convert a result set into JSON.
  613. *
  614. * Part of JsonSerializable interface.
  615. *
  616. * @return array The data to convert to JSON
  617. */
  618. public function jsonSerialize() {
  619. return $this->toArray();
  620. }
  621. /**
  622. * Iterates once all elements in this collection and executes all stacked
  623. * operations of them, finally it returns a new collection with the result.
  624. * This is useful for converting non-rewindable internal iterators into
  625. * a collection that can be rewound and used multiple times.
  626. *
  627. * A common use case is to re-use the same variable for calculating different
  628. * data. In those cases it may be helpful and more performant to first compile
  629. * a collection and then apply more operations to it.
  630. *
  631. * ### Example:
  632. *
  633. * {{{
  634. * $collection->map($mapper)->sortBy('age')->extract('name');
  635. * $compiled = $collection->compile();
  636. * $isJohnHere = $compiled->some($johnMatcher);
  637. * $allButJohn = $compiled->filter($johnMatcher);
  638. * }}}
  639. *
  640. * In the above example, had the collection not been compiled before, the
  641. * iterations for `map`, `sortBy` and `extract` would've been executed twice:
  642. * once for getting `$isJohnHere` and once for `$allButJohn`
  643. *
  644. * You can think of this method as a way to create save points for complex
  645. * calculations in a collection.
  646. *
  647. * @param boolean $preserveKeys whether to use the keys returned by this
  648. * collection as the array keys. Keep in mind that it is valid for iterators
  649. * to return the same key for different elements, setting this value to false
  650. * can help getting all items if keys are not important in the result.
  651. * @return \Cake\Collection\Collection
  652. */
  653. public function compile($preserveKeys = true) {
  654. return new Collection($this->toArray($preserveKeys));
  655. }
  656. }