PageRenderTime 62ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/htdocs/lithium/0.9.9/libraries/lithium/util/Collection.php

http://github.com/pmjones/php-framework-benchmarks
PHP | 532 lines | 173 code | 40 blank | 319 comment | 17 complexity | 0d74400f964a7817715fb3cc552ab1e3 MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0, BSD-3-Clause, ISC, AGPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2010, 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(); // 1 (the first non-empty value)
  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
  73. * @link http://us.php.net/manual/en/class.iterator.php
  74. * @link http://us.php.net/manual/en/class.countable.php
  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. * Indicates whether the current position is valid or not.
  95. *
  96. * @var boolean
  97. * @see lithium\util\Collection::valid()
  98. */
  99. protected $_valid = false;
  100. /**
  101. * Allows a collection's items to be automatically assigned from class construction options.
  102. *
  103. * @var array
  104. */
  105. protected $_autoConfig = array('data');
  106. /**
  107. * Accessor method for adding format handlers to instances and subclasses of `Collection`.
  108. * The values assigned are used by `Collection::to()` to convert `Collection` instances into
  109. * different formats, i.e. JSON.
  110. *
  111. * This can be accomplished in two ways. First, format handlers may be registered on a
  112. * case-by-case basis, as in the following:
  113. *
  114. * {{{
  115. * Collection::formats('json', function($collection, $options) {
  116. * return json_encode($collection->to('array'));
  117. * });
  118. *
  119. * // You can also implement the above as a static class method, and register it as follows:
  120. * Collection::formats('json', '\my\custom\Formatter::toJson');
  121. * }}}
  122. *
  123. * Alternatively, you can implement a class that can handle several formats. This class must
  124. * implement two static methods:
  125. *
  126. * - A `formats()` method, which returns an array indicating what formats it handles.
  127. *
  128. * - A `to()` method, which handles the actual conversion.
  129. *
  130. * Once a class implements these methods, it may be registered per the followng:
  131. * {{{
  132. * Collection::formats('\lithium\net\http\Media');
  133. * }}}
  134. *
  135. * For reference on how to implement these methods, see the `Media` class.
  136. *
  137. * Once a handler is registered, any instance of `Collection` or a subclass can be converted to
  138. * the format(s) supported by the class or handler, using the `to()` method.
  139. *
  140. * @see lithium\net\http\Media::to()
  141. * @see lithium\net\http\Media::formats()
  142. * @see lithium\util\Collection::to()
  143. * @param string $format A string representing the name of the format that a `Collection` can
  144. * be converted to. This corresponds to the `$format` parameter in the `to()`
  145. * method. Alternatively, the fully-namespaced class name of a format-handler
  146. * class.
  147. * @param mixed $handler If `$format` is the name of a format string, `$handler` should be the
  148. * function that handles the conversion, either an anonymous function, or a
  149. * reference to a method name in `"Class::method"` form. If `$format` is a class
  150. * name, can be `null`.
  151. * @return mixed Returns the value of the format handler assigned.
  152. */
  153. public static function formats($format, $handler = null) {
  154. if ($format === false) {
  155. return static::$_formats = array('array' => '\lithium\util\Collection::toArray');
  156. }
  157. if ((is_null($handler)) && class_exists($format)) {
  158. return static::$_formats[] = $format;
  159. }
  160. return static::$_formats[$format] = $handler;
  161. }
  162. /**
  163. * Initializes the collection object by merging in collection items and removing redundant
  164. * object properties.
  165. *
  166. * @return void
  167. */
  168. protected function _init() {
  169. parent::_init();
  170. unset($this->_config['data']);
  171. }
  172. /**
  173. * Handles dispatching of methods against all items in the collection.
  174. *
  175. * @param string $method The name of the method to call on each instance in the collection.
  176. * @param array $params The parameters to pass on each method call.
  177. * @param array $options Specifies options for how to run the given method against the object
  178. * collection. The available options are:
  179. * - `'collect'`: If `true`, the results of this method call will be returned
  180. * wrapped in a new `Collection` object or subclass.
  181. * - `'merge'`: Used primarily if the method being invoked returns an array. If
  182. * set to `true`, merges all results arrays into one.
  183. * @todo Implement filtering.
  184. * @return mixed Returns either an array of the return values of the methods, or the return
  185. * values wrapped in a `Collection` instance.
  186. */
  187. public function invoke($method, array $params = array(), array $options = array()) {
  188. $class = get_class($this);
  189. $defaults = array('merge' => false, 'collect' => false);
  190. $options += $defaults;
  191. $data = array();
  192. foreach ($this as $object) {
  193. $value = call_user_func_array(array(&$object, $method), $params);
  194. ($options['merge']) ? $data = array_merge($data, $value) : $data[$this->key()] = $value;
  195. }
  196. return ($options['collect']) ? new $class(compact('data')) : $data;
  197. }
  198. /**
  199. * Hook to handle dispatching of methods against all items in the collection.
  200. *
  201. * @param string $method
  202. * @param array $parameters
  203. * @return mixed
  204. */
  205. public function __call($method, $parameters = array()) {
  206. return $this->invoke($method, $parameters);
  207. }
  208. /**
  209. * Converts a `Collection` object to another type of object, or a simple type such as an array.
  210. * The supported values of `$format` depend on the format handlers registered in the static
  211. * property `Collection::$_formats`. The `Collection` class comes with built-in support for
  212. * array conversion, but other formats may be registered.
  213. *
  214. * Once the appropriate handlers are registered, a `Collection` instance can be converted into
  215. * any handler-supported format, i.e.: {{{
  216. * $collection->to('json'); // returns a JSON string
  217. * $collection->to('xml'); // returns an XML string
  218. * }}}
  219. *
  220. * _Please note that Lithium does not ship with a default XML handler, but one can be
  221. * configured easily._
  222. *
  223. * @see lithium\util\Collection::formats()
  224. * @see lithium\util\Collection::$_formats
  225. * @param string $format By default the only supported value is `'array'`. However, additional
  226. * format handlers can be registered using the `formats()` method.
  227. * @param $options Options for converting this collection:
  228. * - `'internal'` _boolean_: Indicates whether the current internal representation of the
  229. * collection should be exported. Defaults to `false`, which uses the standard iterator
  230. * interfaces. This is useful for exporting record sets, where records are lazy-loaded,
  231. * and the collection must be iterated in order to fetch all objects.
  232. * @return mixed The object converted to the value specified in `$format`; usually an array or
  233. * string.
  234. */
  235. public function to($format, array $options = array()) {
  236. $defaults = array('internal' => false);
  237. $options += $defaults;
  238. $data = $options['internal'] ? $this->_data : $this;
  239. if (is_object($format) && is_callable($format)) {
  240. return $format($data, $options);
  241. }
  242. if (isset(static::$_formats[$format]) && is_callable(static::$_formats[$format])) {
  243. $handler = static::$_formats[$format];
  244. $handler = is_string($handler) ? explode('::', $handler, 2) : $handler;
  245. if (is_array($handler)) {
  246. list($class, $method) = $handler;
  247. return $class::$method($data, $options);
  248. }
  249. return $handler($data, $options);
  250. }
  251. foreach (static::$_formats as $key => $handler) {
  252. if (!is_int($key)) {
  253. continue;
  254. }
  255. if (in_array($format, $handler::formats($format, $data, $options))) {
  256. return $handler::to($format, $data, $options);
  257. }
  258. }
  259. }
  260. /**
  261. * Filters a copy of the items in the collection.
  262. *
  263. * @param callback $filter Callback to use for filtering.
  264. * @param array $options The available options are:
  265. * - `'collect'`: If `true`, the results will be returned wrapped
  266. * in a new `Collection` object or subclass.
  267. * @return array|object The filtered items.
  268. */
  269. public function find($filter, array $options = array()) {
  270. $defaults = array('collect' => true);
  271. $options += $defaults;
  272. $data = array_filter($this->_data, $filter);
  273. if ($options['collect']) {
  274. $class = get_class($this);
  275. $data = new $class(compact('data'));
  276. }
  277. return $data;
  278. }
  279. /**
  280. * Returns the first non-empty value in the collection after a filter is applied, or rewinds the
  281. * collection and returns the first value.
  282. *
  283. * @see lithium\util\Collection::rewind()
  284. * @param callback $filter A closure through which collection values will be
  285. * passed. If the return value of this function is non-empty,
  286. * it will be returned as the result of the method call. If `null`, the
  287. * collection is rewound (see `rewind()`) and the first item is returned.
  288. * @return mixed Returns the first non-empty collection value returned from `$filter`.
  289. */
  290. public function first($filter = null) {
  291. if (!$filter) {
  292. return $this->rewind();
  293. }
  294. foreach ($this as $item) {
  295. if ($filter($item)) {
  296. return $item;
  297. }
  298. }
  299. }
  300. /**
  301. * Applies a callback to all items in the collection.
  302. *
  303. * @param callback $filter The filter to apply.
  304. * @return object This collection instance.
  305. */
  306. public function each($filter) {
  307. $this->_data = array_map($filter, $this->_data);
  308. return $this;
  309. }
  310. /**
  311. * Applies a callback to a copy of all data in the collection
  312. * and returns the result.
  313. *
  314. * @param callback $filter The filter to apply.
  315. * @param array $options The available options are:
  316. * - `'collect'`: If `true`, the results will be returned wrapped
  317. * in a new `Collection` object or subclass.
  318. * @return array|object The filtered data.
  319. */
  320. public function map($filter, array $options = array()) {
  321. $defaults = array('collect' => true);
  322. $options += $defaults;
  323. $data = array_map($filter, $this->_data);
  324. if ($options['collect']) {
  325. $class = get_class($this);
  326. return new $class(compact('data'));
  327. }
  328. return $data;
  329. }
  330. /**
  331. * Checks whether or not an offset exists.
  332. *
  333. * @param string $offset An offset to check for.
  334. * @return boolean `true` if offset exists, `false` otherwise.
  335. */
  336. public function offsetExists($offset) {
  337. return isset($this->_data[$offset]);
  338. }
  339. /**
  340. * Returns the value at specified offset.
  341. *
  342. * @param string $offset The offset to retrieve.
  343. * @return mixed Value at offset.
  344. */
  345. public function offsetGet($offset) {
  346. return $this->_data[$offset];
  347. }
  348. /**
  349. * Assigns a value to the specified offset.
  350. *
  351. * @param string $offset The offset to assign the value to.
  352. * @param mixed $value The value to set.
  353. * @return mixed The value which was set.
  354. */
  355. public function offsetSet($offset, $value) {
  356. if (is_null($offset)) {
  357. return $this->_data[] = $value;
  358. }
  359. return $this->_data[$offset] = $value;
  360. }
  361. /**
  362. * Unsets an offset.
  363. *
  364. * @param string $offset The offset to unset.
  365. * @return void
  366. */
  367. public function offsetUnset($offset) {
  368. unset($this->_data[$offset]);
  369. }
  370. /**
  371. * Rewinds to the first item.
  372. *
  373. * @return mixed The current item after rewinding.
  374. */
  375. public function rewind() {
  376. $this->_valid = (reset($this->_data) !== false);
  377. return current($this->_data);
  378. }
  379. /**
  380. * Moves forward to the last item.
  381. *
  382. * @return mixed The current item after moving.
  383. */
  384. public function end() {
  385. $this->_valid = (end($this->_data) !== false);
  386. return current($this->_data);
  387. }
  388. /**
  389. * Checks if current position is valid.
  390. *
  391. * @return boolean `true` if valid, `false` otherwise.
  392. */
  393. public function valid() {
  394. return $this->_valid;
  395. }
  396. /**
  397. * Returns the current item.
  398. *
  399. * @return mixed The current item.
  400. */
  401. public function current() {
  402. return current($this->_data);
  403. }
  404. /**
  405. * Returns the key of the current item.
  406. *
  407. * @return scalar Scalar on success `0` on failure.
  408. */
  409. public function key() {
  410. return key($this->_data);
  411. }
  412. /**
  413. * Moves backward to the previous item. If already at the first item,
  414. * moves to the last one.
  415. *
  416. * @return mixed The current item after moving.
  417. */
  418. public function prev() {
  419. if (!prev($this->_data)) {
  420. end($this->_data);
  421. }
  422. return current($this->_data);
  423. }
  424. /**
  425. * Move forwards to the next item.
  426. *
  427. * @return The current item after moving.
  428. */
  429. public function next() {
  430. $this->_valid = (next($this->_data) !== false);
  431. return current($this->_data);
  432. }
  433. /**
  434. * Appends an item.
  435. *
  436. * @param mixed $value The item to append.
  437. * @return void
  438. */
  439. public function append($value) {
  440. is_object($value) ? $this->_data[] =& $value : $this->_data[] = $value;
  441. }
  442. /**
  443. * Counts the items of the object.
  444. *
  445. * @return integer Returns the number of items in the collection.
  446. */
  447. public function count() {
  448. $count = iterator_count($this);
  449. $this->rewind();
  450. return $count;
  451. }
  452. /**
  453. * Returns the item keys.
  454. *
  455. * @return array The keys of the items.
  456. */
  457. public function keys() {
  458. return array_keys($this->_data);
  459. }
  460. /**
  461. * Exports a `Collection` instance to an array. Used by `Collection::to()`.
  462. *
  463. * @param mixed $data Either a `Collection` instance, or an array representing a `Collection`'s
  464. * internal state.
  465. * @return array Returns the value of `$data` as a pure PHP array, recursively converting all
  466. * sub-objects and other values to their closest array or scalar equivalents.
  467. */
  468. public static function toArray($data) {
  469. $result = array();
  470. foreach ($data as $key => $item) {
  471. switch (true) {
  472. case (!is_object($item)):
  473. $result[$key] = $item;
  474. break;
  475. case (method_exists($item, 'to')):
  476. $result[$key] = $item->to('array');
  477. break;
  478. case ($vars = get_object_vars($item)):
  479. $result[$key] = $vars;
  480. break;
  481. case (method_exists($item, '__toString')):
  482. $result[$key] = (string) $item;
  483. break;
  484. default:
  485. $result[$key] = $item;
  486. break;
  487. }
  488. }
  489. return $result;
  490. }
  491. }
  492. ?>