Collection.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\util;
  9. /**
  10. * The parent class for all collection objects. Contains methods for collection iteration,
  11. * conversion, and filtering. Implements `ArrayAccess`, `Iterator`, and `Countable`.
  12. *
  13. * Collection objects can act very much like arrays. This is especially evident in creating new
  14. * objects, or by converting Collection into an actual array:
  15. *
  16. * {{{
  17. * $coll = new Collection();
  18. * $coll[] = 'foo';
  19. * // $coll[0] --> 'foo'
  20. *
  21. * $coll = new Collection(array('data' => array('foo')));
  22. * // $coll[0] --> 'foo'
  23. *
  24. * $array = $coll->to('array');
  25. * }}}
  26. *
  27. * Apart from array-like data access, Collections allow for filtering and iteration methods:
  28. *
  29. * {{{
  30. *
  31. * $coll = new Collection(array('data' => array(0, 1, 2, 3, 4)));
  32. *
  33. * $coll->first(); // 0
  34. * $coll->current(); // 0
  35. * $coll->next(); // 1
  36. * $coll->next(); // 2
  37. * $coll->next(); // 3
  38. * $coll->prev(); // 2
  39. * $coll->rewind(); // 0
  40. * }}}
  41. *
  42. * The primary purpose of the `Collection` class is to enable simple, efficient access to groups
  43. * of similar objects, and to perform operations against these objects using anonymous functions.
  44. *
  45. * The `map()` and `each()` methods allow you to perform operations against the entire set of values
  46. * in a `Collection`, while `find()` and `first()` allow you to search through values and pick out
  47. * one or more.
  48. *
  49. * The `Collection` class also supports dispatching methods against a set of objects, if the method
  50. * is supported by all objects. For example: {{{
  51. * class Task {
  52. * public function run($when) {
  53. * // Do some work
  54. * }
  55. * }
  56. *
  57. * $data = array(
  58. * new Task(array('task' => 'task 1')),
  59. * new Task(array('task' => 'task 2')),
  60. * new Task(array('task' => 'task 3'))
  61. * );
  62. * $tasks = new Collection(compact('data'));
  63. *
  64. * // $result will contain an array, and each element will be the return
  65. * // value of a run() method call:
  66. * $result = $tasks->invoke('run', array('now'));
  67. *
  68. * // Alternatively, the method can be called natively, with the same result:
  69. * $result = $tasks->run('now');
  70. * }}}
  71. *
  72. * @link http://us.php.net/manual/en/class.arrayaccess.php PHP Manual: ArrayAccess Interface
  73. * @link http://us.php.net/manual/en/class.iterator.php PHP Manual: Iterator Interface
  74. * @link http://us.php.net/manual/en/class.countable.php PHP Manual: Countable Interface
  75. */
  76. class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator, \Countable {
  77. /**
  78. * A central registry of global format handlers for `Collection` objects and subclasses.
  79. * Accessed via the `formats()` method.
  80. *
  81. * @see lithium\util\Collection::formats()
  82. * @var array
  83. */
  84. protected static $_formats = array(
  85. 'array' => 'lithium\util\Collection::toArray'
  86. );
  87. /**
  88. * The items contained in the collection.
  89. *
  90. * @var array
  91. */
  92. protected $_data = array();
  93. /**
  94. * Allows a collection's items to be automatically assigned from class construction options.
  95. *
  96. * @var array
  97. */
  98. protected $_autoConfig = array('data');
  99. /**
  100. * Accessor method for adding format handlers to instances and subclasses of `Collection`.
  101. * The values assigned are used by `Collection::to()` to convert `Collection` instances into
  102. * different formats, i.e. JSON.
  103. *
  104. * This can be accomplished in two ways. First, format handlers may be registered on a
  105. * case-by-case basis, as in the following:
  106. *
  107. * {{{
  108. * Collection::formats('json', function($collection, $options) {
  109. * return json_encode($collection->to('array'));
  110. * });
  111. *
  112. * // You can also implement the above as a static class method, and register it as follows:
  113. * Collection::formats('json', '\my\custom\Formatter::toJson');
  114. * }}}
  115. *
  116. * Alternatively, you can implement a class that can handle several formats. This class must
  117. * implement two static methods:
  118. *
  119. * - A `formats()` method, which returns an array indicating what formats it handles.
  120. *
  121. * - A `to()` method, which handles the actual conversion.
  122. *
  123. * Once a class implements these methods, it may be registered per the following:
  124. * {{{
  125. * Collection::formats('\lithium\net\http\Media');
  126. * }}}
  127. *
  128. * For reference on how to implement these methods, see the `Media` class.
  129. *
  130. * Once a handler is registered, any instance of `Collection` or a subclass can be converted to
  131. * the format(s) supported by the class or handler, using the `to()` method.
  132. *
  133. * @see lithium\net\http\Media::to()
  134. * @see lithium\net\http\Media::formats()
  135. * @see lithium\util\Collection::to()
  136. * @param string $format A string representing the name of the format that a `Collection` can
  137. * be converted to. This corresponds to the `$format` parameter in the `to()`
  138. * method. Alternatively, the fully-namespaced class name of a format-handler
  139. * class.
  140. * @param mixed $handler If `$format` is the name of a format string, `$handler` should be the
  141. * function that handles the conversion, either an anonymous function, or a
  142. * reference to a method name in `"Class::method"` form. If `$format` is a class
  143. * name, can be `null`.
  144. * @return mixed Returns the value of the format handler assigned.
  145. */
  146. public static function formats($format, $handler = null) {
  147. if ($format === false) {
  148. return static::$_formats = array('array' => 'lithium\util\Collection::toArray');
  149. }
  150. if ((is_null($handler)) && class_exists($format)) {
  151. return static::$_formats[] = $format;
  152. }
  153. return static::$_formats[$format] = $handler;
  154. }
  155. /**
  156. * Initializes the collection object by merging in collection items and removing redundant
  157. * object properties.
  158. *
  159. * @return void
  160. */
  161. protected function _init() {
  162. parent::_init();
  163. unset($this->_config['data']);
  164. }
  165. /**
  166. * Handles dispatching of methods against all items in the collection.
  167. *
  168. * @param string $method The name of the method to call on each instance in the collection.
  169. * @param array $params The parameters to pass on each method call.
  170. * @param array $options Specifies options for how to run the given method against the object
  171. * collection. The available options are:
  172. * - `'collect'`: If `true`, the results of this method call will be returned
  173. * wrapped in a new `Collection` object or subclass.
  174. * - `'merge'`: Used primarily if the method being invoked returns an array. If
  175. * set to `true`, merges all results arrays into one.
  176. * @todo Implement filtering.
  177. * @return mixed Returns either an array of the return values of the methods, or the return
  178. * values wrapped in a `Collection` instance.
  179. */
  180. public function invoke($method, array $params = array(), array $options = array()) {
  181. $class = get_class($this);
  182. $defaults = array('merge' => false, 'collect' => false);
  183. $options += $defaults;
  184. $data = array();
  185. foreach ($this as $object) {
  186. $value = call_user_func_array(array(&$object, $method), $params);
  187. ($options['merge']) ? $data = array_merge($data, $value) : $data[$this->key()] = $value;
  188. }
  189. return ($options['collect']) ? new $class(compact('data')) : $data;
  190. }
  191. /**
  192. * Hook to handle dispatching of methods against all items in the collection.
  193. *
  194. * @param string $method
  195. * @param array $parameters
  196. * @return mixed
  197. */
  198. public function __call($method, $parameters = array()) {
  199. return $this->invoke($method, $parameters);
  200. }
  201. /**
  202. * Custom check to determine if our given magic methods can be responded to.
  203. *
  204. * @param string $method Method name.
  205. * @param bool $internal Interal call or not.
  206. * @return bool
  207. */
  208. public function respondsTo($method, $internal = false) {
  209. $magicMethod = count($this->_data) > 0 && $this->_data[0]->respondsTo($method, $internal);
  210. return $magicMethod || parent::respondsTo($method, $internal);
  211. }
  212. /**
  213. * Converts a `Collection` object to another type of object, or a simple type such as an array.
  214. * The supported values of `$format` depend on the format handlers registered in the static
  215. * property `Collection::$_formats`. The `Collection` class comes with built-in support for
  216. * array conversion, but other formats may be registered.
  217. *
  218. * Once the appropriate handlers are registered, a `Collection` instance can be converted into
  219. * any handler-supported format, i.e.: {{{
  220. * $collection->to('json'); // returns a JSON string
  221. * $collection->to('xml'); // returns an XML string
  222. * }}}
  223. *
  224. * _Please note that Lithium does not ship with a default XML handler, but one can be
  225. * configured easily._
  226. *
  227. * @see lithium\util\Collection::formats()
  228. * @see lithium\util\Collection::$_formats
  229. * @param string $format By default the only supported value is `'array'`. However, additional
  230. * format handlers can be registered using the `formats()` method.
  231. * @param array $options Options for converting this collection:
  232. * - `'internal'` _boolean_: Indicates whether the current internal representation of the
  233. * collection should be exported. Defaults to `false`, which uses the standard iterator
  234. * interfaces. This is useful for exporting record sets, where records are lazy-loaded,
  235. * and the collection must be iterated in order to fetch all objects.
  236. * @return mixed The object converted to the value specified in `$format`; usually an array or
  237. * string.
  238. */
  239. public function to($format, array $options = array()) {
  240. $defaults = array('internal' => false);
  241. $options += $defaults;
  242. $data = $options['internal'] ? $this->_data : $this;
  243. return $this->_to($format, $data, $options);
  244. }
  245. protected function _to($format, &$data, &$options) {
  246. if (is_object($format) && is_callable($format)) {
  247. return $format($data, $options);
  248. }
  249. if (isset(static::$_formats[$format]) && is_callable(static::$_formats[$format])) {
  250. $handler = static::$_formats[$format];
  251. $handler = is_string($handler) ? explode('::', $handler, 2) : $handler;
  252. if (is_array($handler)) {
  253. list($class, $method) = $handler;
  254. return $class::$method($data, $options);
  255. }
  256. return $handler($data, $options);
  257. }
  258. foreach (static::$_formats as $key => $handler) {
  259. if (!is_int($key)) {
  260. continue;
  261. }
  262. if (in_array($format, $handler::formats($format, $data, $options))) {
  263. return $handler::to($format, $data, $options);
  264. }
  265. }
  266. }
  267. /**
  268. * Filters a copy of the items in the collection.
  269. *
  270. * @param callback $filter Callback to use for filtering.
  271. * @param array $options The available options are:
  272. * - `'collect'`: If `true`, the results will be returned wrapped
  273. * in a new `Collection` object or subclass.
  274. * @return mixed The filtered items. Will be an array unless `'collect'` is defined in the
  275. * `$options` argument, then an instance of this class will be returned.
  276. */
  277. public function find($filter, array $options = array()) {
  278. $defaults = array('collect' => true);
  279. $options += $defaults;
  280. $data = array_filter($this->_data, $filter);
  281. if ($options['collect']) {
  282. $class = get_class($this);
  283. $data = new $class(compact('data'));
  284. }
  285. return $data;
  286. }
  287. /**
  288. * Returns the first non-empty value in the collection after a filter is applied, or rewinds the
  289. * collection and returns the first value.
  290. *
  291. * @see lithium\util\Collection::rewind()
  292. * @param callback $filter A closure through which collection values will be
  293. * passed. If the return value of this function is non-empty,
  294. * it will be returned as the result of the method call. If `null`, the
  295. * collection is rewound (see `rewind()`) and the first item is returned.
  296. * @return mixed Returns the first non-empty collection value returned from `$filter`.
  297. */
  298. public function first($filter = null) {
  299. if (!$filter) {
  300. return $this->rewind();
  301. }
  302. foreach ($this as $item) {
  303. if ($filter($item)) {
  304. return $item;
  305. }
  306. }
  307. }
  308. /**
  309. * Applies a callback to all items in the collection.
  310. *
  311. * @param callback $filter The filter to apply.
  312. * @return object This collection instance.
  313. */
  314. public function each($filter) {
  315. $this->_data = array_map($filter, $this->_data);
  316. return $this;
  317. }
  318. /**
  319. * Applies a callback to a copy of all data in the collection
  320. * and returns the result.
  321. *
  322. * @param callback $filter The filter to apply.
  323. * @param array $options The available options are:
  324. * - `'collect'`: If `true`, the results will be returned wrapped
  325. * in a new `Collection` object or subclass.
  326. * @return mixed The filtered items. Will be an array unless `'collect'` is defined in the
  327. * `$options` argument, then an instance of this class will be returned.
  328. */
  329. public function map($filter, array $options = array()) {
  330. $defaults = array('collect' => true);
  331. $options += $defaults;
  332. $data = array_map($filter, $this->_data);
  333. if ($options['collect']) {
  334. $class = get_class($this);
  335. return new $class(compact('data'));
  336. }
  337. return $data;
  338. }
  339. /**
  340. * Reduce, or fold, a collection down to a single value
  341. *
  342. * @param callback $filter The filter to apply.
  343. * @param mixed $initial Initial value
  344. * @return mixed A single reduced value
  345. */
  346. public function reduce($filter, $initial = false) {
  347. return array_reduce($this->_data, $filter, $initial);
  348. }
  349. /**
  350. * Sorts the objects in the collection.
  351. *
  352. * @param callable $sorter The sorter for the data, can either be a sort function like
  353. * natsort or a compare function like strcmp.
  354. * @param array $options The available options are:
  355. * - No options yet implemented
  356. * @return $this, useful for chaining this with other methods.
  357. */
  358. public function sort($sorter = 'sort', array $options = array()) {
  359. if (is_string($sorter) && strpos($sorter, 'sort') !== false && is_callable($sorter)) {
  360. call_user_func_array($sorter, array(&$this->_data));
  361. } else if (is_callable($sorter)) {
  362. usort($this->_data, $sorter);
  363. }
  364. return $this;
  365. }
  366. /**
  367. * Checks whether or not an offset exists.
  368. *
  369. * @param string $offset An offset to check for.
  370. * @return boolean `true` if offset exists, `false` otherwise.
  371. */
  372. public function offsetExists($offset) {
  373. return array_key_exists($offset, $this->_data);
  374. }
  375. /**
  376. * Returns the value at specified offset.
  377. *
  378. * @param string $offset The offset to retrieve.
  379. * @return mixed Value at offset.
  380. */
  381. public function offsetGet($offset) {
  382. return $this->_data[$offset];
  383. }
  384. /**
  385. * Assigns a value to the specified offset.
  386. *
  387. * @param string $offset The offset to assign the value to.
  388. * @param mixed $value The value to set.
  389. * @return mixed The value which was set.
  390. */
  391. public function offsetSet($offset, $value) {
  392. if (is_null($offset)) {
  393. return $this->_data[] = $value;
  394. }
  395. return $this->_data[$offset] = $value;
  396. }
  397. /**
  398. * Unsets an offset.
  399. *
  400. * @param string $offset The offset to unset.
  401. * @return void
  402. */
  403. public function offsetUnset($offset) {
  404. prev($this->_data);
  405. if (key($this->_data) === null) {
  406. $this->rewind();
  407. }
  408. unset($this->_data[$offset]);
  409. }
  410. /**
  411. * Rewinds to the first item.
  412. *
  413. * @return mixed The current item after rewinding.
  414. */
  415. public function rewind() {
  416. reset($this->_data);
  417. return current($this->_data);
  418. }
  419. /**
  420. * Moves forward to the last item.
  421. *
  422. * @return mixed The current item after moving.
  423. */
  424. public function end() {
  425. end($this->_data);
  426. return current($this->_data);
  427. }
  428. /**
  429. * Checks if current position is valid.
  430. *
  431. * @return boolean `true` if valid, `false` otherwise.
  432. */
  433. public function valid() {
  434. return key($this->_data) !== null;
  435. }
  436. /**
  437. * Returns the current item.
  438. *
  439. * @return mixed The current item or `false` on failure.
  440. */
  441. public function current() {
  442. return current($this->_data);
  443. }
  444. /**
  445. * Returns the key of the current item.
  446. *
  447. * @return scalar Scalar on success or `null` on failure.
  448. */
  449. public function key() {
  450. return key($this->_data);
  451. }
  452. /**
  453. * Moves backward to the previous item. If already at the first item,
  454. * moves to the last one.
  455. *
  456. * @return mixed The current item after moving or the last item on failure.
  457. */
  458. public function prev() {
  459. if (!prev($this->_data)) {
  460. end($this->_data);
  461. }
  462. return current($this->_data);
  463. }
  464. /**
  465. * Move forwards to the next item.
  466. *
  467. * @return The current item after moving or `false` on failure.
  468. */
  469. public function next() {
  470. next($this->_data);
  471. return current($this->_data);
  472. }
  473. /**
  474. * Appends an item.
  475. *
  476. * @param mixed $value The item to append.
  477. * @return void
  478. */
  479. public function append($value) {
  480. is_object($value) ? $this->_data[] =& $value : $this->_data[] = $value;
  481. }
  482. /**
  483. * Counts the items of the object.
  484. *
  485. * @return integer Returns the number of items in the collection.
  486. */
  487. public function count() {
  488. $count = iterator_count($this);
  489. $this->rewind();
  490. return $count;
  491. }
  492. /**
  493. * Returns the item keys.
  494. *
  495. * @return array The keys of the items.
  496. */
  497. public function keys() {
  498. return array_keys($this->_data);
  499. }
  500. /**
  501. * Exports a `Collection` instance to an array. Used by `Collection::to()`.
  502. *
  503. * @param mixed $data Either a `Collection` instance, or an array representing a `Collection`'s
  504. * internal state.
  505. * @param array $options Options used when converting `$data` to an array:
  506. * - `'handlers'` _array_: An array where the keys are fully-namespaced class
  507. * names, and the values are closures that take an instance of the class as a
  508. * parameter, and return an array or scalar value that the instance represents.
  509. * @return array Returns the value of `$data` as a pure PHP array, recursively converting all
  510. * sub-objects and other values to their closest array or scalar equivalents.
  511. */
  512. public static function toArray($data, array $options = array()) {
  513. $defaults = array('handlers' => array());
  514. $options += $defaults;
  515. $result = array();
  516. foreach ($data as $key => $item) {
  517. switch (true) {
  518. case is_array($item):
  519. $result[$key] = static::toArray($item, $options);
  520. break;
  521. case (!is_object($item)):
  522. $result[$key] = $item;
  523. break;
  524. case (isset($options['handlers'][$class = get_class($item)])):
  525. $result[$key] = $options['handlers'][$class]($item);
  526. break;
  527. case (method_exists($item, 'to')):
  528. $result[$key] = $item->to('array', $options);
  529. break;
  530. case ($vars = get_object_vars($item)):
  531. $result[$key] = static::toArray($vars, $options);
  532. break;
  533. case (method_exists($item, '__toString')):
  534. $result[$key] = (string) $item;
  535. break;
  536. default:
  537. $result[$key] = $item;
  538. break;
  539. }
  540. }
  541. return $result;
  542. }
  543. }
  544. ?>