PageRenderTime 245ms CodeModel.GetById 179ms app.highlight 7ms RepoModel.GetById 55ms app.codeStats 0ms

/lithium-0.6/libraries/lithium/util/Collection.php

https://github.com/gwoo/framework-benchs
PHP | 460 lines | 185 code | 43 blank | 232 comment | 23 complexity | 1b5a4e41ce4ae842849b5d308f6c951c MD5 | raw file
  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
  9namespace lithium\util;
 10
 11/**
 12 * The parent class for all collection objects. Contains methods for collection iteration, 
 13 * conversion, and filtering. Implements `ArrayAccess`, `Iterator`, and `Countable`.
 14 *
 15 * Collection objects can act very much like arrays. This is especially evident in creating new
 16 * objects, or by converting Collection into an actual array:
 17 *
 18 * {{{
 19 * $coll = new Collection();
 20 * $coll[] = 'foo';
 21 * // $coll[0] --> 'foo'
 22 *
 23 * $coll = new Collection(array('items' => array('foo')));
 24 * // $coll[0] --> 'foo'
 25 * 
 26 * $array = $coll->to('array);
 27 * }}}
 28 *
 29 * Apart from array-like data access, Collections allow for filtering and iteration methods:
 30 * 
 31 * {{{
 32 *
 33 * $coll = new Collection(array('items' => array(0, 1, 2, 3, 4)));
 34 *
 35 * $coll->first();   // 1 (the first non-empty value)
 36 * $coll->current(); // 0
 37 * $coll->next();    // 1
 38 * $coll->next();    // 2
 39 * $coll->next();    // 3
 40 * $coll->prev();    // 2
 41 * $coll->rewind();  // 0
 42 * }}}
 43 *
 44 * @link http://us.php.net/manual/en/class.arrayaccess.php
 45 * @link http://us.php.net/manual/en/class.iterator.php
 46 * @link http://us.php.net/manual/en/class.countable.php
 47 */
 48class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator, \Countable {
 49
 50	/**
 51	 * A central registry of global format handlers for `Collection` objects and subclasses.
 52	 * Accessed via the `formats()` method.
 53	 *
 54	 * @see \lithium\util\Collection::formats()
 55	 * @var array
 56	 */
 57	protected static $_formats = array(
 58		'array' => '\lithium\util\Collection::_toArray'
 59	);
 60
 61	/**
 62	 * The items contained in the collection.
 63	 *
 64	 * @var array
 65	 */
 66	protected $_items = array();
 67
 68	/**
 69	 * Indicates whether the current position is valid or not.
 70	 *
 71	 * @var boolean
 72	 * @see lithium\util\Collection::valid()
 73	 */
 74	protected $_valid = false;
 75
 76	/**
 77	 * Allows a collection's items to be automatically assigned from class construction options.
 78	 *
 79	 * @var array
 80	 */
 81	protected $_autoConfig = array('items');
 82
 83	/**
 84	 * Accessor method for adding format handlers to instances and subclasses of `Collection`.
 85	 *
 86	 * @param string $format
 87	 * @param mixed $handler
 88	 * @return mixed
 89	 */
 90	public static function formats($format, $handler = null) {
 91		if ($format === false) {
 92			return static::$_formats = array();
 93		}
 94		if ((is_null($handler)) && class_exists($format)) {
 95			return static::$_formats[] = $format;
 96		}
 97		return static::$_formats[$format] = $handler;
 98	}
 99
100	/**
101	 * Initializes the collection object by merging in collection items and removing redundant
102	 * object properties.
103	 *
104	 * @return void
105	 */
106	protected function _init() {
107		parent::_init();
108		unset($this->_config['items']);
109	}
110
111	/**
112	 * Handles dispatching of methods against all items in the collection.
113	 *
114	 * @param string $method
115	 * @param array $parameters
116	 * @param array $options Specifies options for how to run the given method against the object
117	 *              collection. The available options are:
118	 *              - `'collect'`: If `true`, the results of this method call will be returned
119	 *                wrapped in a new Collection object or subclass.
120	 *              - `'merge'`: Used primarily if the method being invoked returns an array.  If
121	 *                set to `true`, merges all results arrays into one.
122	 * @todo Implement filtering.
123	 * @return mixed
124	 */
125	public function invoke($method, $parameters = array(), $options = array()) {
126		$defaults = array('merge' => false, 'collect' => false);
127		$options += $defaults;
128		$results = array();
129		$isCore = null;
130
131		foreach ($this->_items as $key => $value) {
132			if (is_null($isCore)) {
133				$isCore = (method_exists(current($this->_items), 'invokeMethod'));
134			}
135
136			if ($isCore) {
137				$result = $this->_items[$key]->invokeMethod($method, $parameters);
138			} else {
139				$result = call_user_func_array(array(&$this->_items[$key], $method), $parameters);
140			}
141
142			if (!empty($options['merge'])) {
143				$results = array_merge($results, $result);
144			} else {
145				$results[$key] = $result;
146			}
147		}
148
149		if ($options['collect']) {
150			$class = get_class($this);
151			$results = new $class(array('items' => $results));
152		}
153		return $results;
154	}
155
156	/**
157	 * Hook to handle dispatching of methods against all items in the collection.
158	 *
159	 * @param string $method
160	 * @param array $parameters
161	 * @return mixed
162	 */
163	public function __call($method, $parameters = array()) {
164		return $this->invoke($method, $parameters);
165	}
166
167	/**
168	 * Converts the Collection object to another type of object, or a simple type such as an array.
169	 *
170	 * @param string $format Currently only `'array'` is supported.
171	 * @param $options Options for converting this collection:
172	 *        - 'internal': Boolean indicating whether the current internal representation of the
173	 *          collection should be exported. Defaults to `false`, which uses the standard iterator
174	 *          interfaces. This is useful for exporting record sets, where records are lazy-loaded,
175	 *          and the collection must be iterated in order to fetch all objects.
176	 * @return mixed The converted object.
177	 */
178	public function to($format, $options = array()) {
179		$defaults = array('internal' => false);
180		$options += $defaults;
181		$data = $options['internal'] ? $this->_items : $this;
182
183		if (is_object($format) && is_callable($format)) {
184			return $format($data, $options);
185		}
186
187		if (isset(static::$_formats[$format]) && is_callable(static::$_formats[$format])) {
188			$handler = static::$_formats[$format];
189			$handler = is_string($handler) ? explode('::', $handler, 2) : $handler;
190
191			if (is_array($handler)) {
192				list($class, $method) = $handler;
193				return $class::$method($data, $options);
194			}
195			return $handler($data, $options);
196		}
197
198		foreach (static::$_formats as $key => $handler) {
199			if (!is_int($key)) {
200				continue;
201			}
202			if (in_array($format, $handler::formats($format, $data, $options))) {
203				return $handler::to($format, $data, $options);
204			}
205		}
206	}
207
208	/**
209	 * Filters a copy of the items in the collection.
210	 *
211	 * @param callback $filter Callback to use for filtering.
212	 * @param array $options The available options are:
213	 *              - `'collect'`: If `true`, the results will be returned wrapped
214	 *              in a new Collection object or subclass.
215	 * @return array|object The filtered items.
216	 */
217	public function find($filter, $options = array()) {
218		$defaults = array('collect' => true);
219		$options += $defaults;
220		$items = array_filter($this->_items, $filter);
221
222		if ($options['collect']) {
223			$class = get_class($this);
224			$items = new $class(compact('items'));
225		}
226		return $items;
227	}
228
229	/**
230	 * Returns the first non-empty value in the collection after a filter is applied, or rewinds the
231	 * collection and returns the first value.
232	 *
233	 * @param callback $filter A closure through which collection values will be
234	 *                 passed. If the return value of this function is non-empty,
235	 *                 it will be returned as the result of the method call. If `null`, the
236	 *                 collection is rewound (see `rewind()`) and the first item is returned.
237	 * @return mixed Returns the first non-empty collection value returned from `$filter`.
238	 * @see lithium\util\Collection::rewind()
239	 */
240	public function first($filter = null) {
241		if (empty($filter)) {
242			return $this->rewind();
243		}
244
245		foreach ($this->_items as $item) {
246			if ($value = $filter($item)) {
247				return $value;
248			}
249		}
250	}
251
252	/**
253	 * Applies a callback to all items in the collection.
254	 *
255	 * @param callback $filter The filter to apply.
256	 * @return object This collection instance.
257	 */
258	public function each($filter) {
259		$this->_items = array_map($filter, $this->_items);
260		return $this;
261	}
262
263	/**
264	 * Applies a callback to a copy of all items in the collection
265	 * and returns the result.
266	 *
267	 * @param callback $filter The filter to apply.
268	 * @param array $options The available options are:
269	 *              - `'collect'`: If `true`, the results will be returned wrapped
270	 *              in a new Collection object or subclass.
271	 * @return array|object The filtered items.
272	 */
273	public function map($filter, $options = array()) {
274		$defaults = array('collect' => true);
275		$options += $defaults;
276		$items = array_map($filter, $this->_items);
277
278		if ($options['collect']) {
279			$class = get_class($this);
280			return new $class(compact('items'));
281		}
282		return $items;
283	}
284
285	/**
286	 * Checks whether or not an offset exists.
287	 *
288	 * @param string $offset An offset to check for.
289	 * @return boolean `true` if offset exists, `false` otherwise.
290	 */
291	public function offsetExists($offset) {
292		return isset($this->_items[$offset]);
293	}
294
295	/**
296	 * Returns the value at specified offset.
297	 *
298	 * @param string $offset The offset to retrieve.
299	 * @return mixed Value at offset.
300	 */
301	public function offsetGet($offset) {
302		return $this->_items[$offset];
303	}
304
305	/**
306	 * Assigns a value to the specified offset.
307	 *
308	 * @param string $offset The offset to assign the value to.
309	 * @param mixed $value The value to set.
310	 * @return mixed The value which was set.
311	 */
312	public function offsetSet($offset, $value) {
313		if (is_null($offset)) {
314			return $this->_items[] = $value;
315		}
316		return $this->_items[$offset] = $value;
317	}
318
319	/**
320	 * Unsets an offset.
321	 *
322	 * @param string $offset The offset to unset.
323	 * @return void
324	 */
325	public function offsetUnset($offset) {
326		unset($this->_items[$offset]);
327	}
328
329	/**
330	 * Rewinds to the first item.
331	 *
332	 * @return mixed The current item after rewinding.
333	 */
334	public function rewind() {
335		$this->_valid = (reset($this->_items) !== false);
336		return current($this->_items);
337	}
338
339	/**
340	 * Moves forward to the last item.
341	 *
342	 * @return mixed The current item after moving.
343	 */
344	public function end() {
345		$this->_valid = (end($this->_items) !== false);
346		return current($this->_items);
347	}
348
349	/**
350	 * Checks if current position is valid.
351	 *
352	 * @return boolean `true` if valid, `false` otherwise.
353	 */
354	public function valid() {
355		return $this->_valid;
356	}
357
358	/**
359	 * Returns the current item.
360	 *
361	 * @return mixed The current item.
362	 */
363	public function current() {
364		return current($this->_items);
365	}
366
367	/**
368	 * Returns the key of the current item.
369	 *
370	 * @return scalar Scalar on success `0` on failure.
371	 */
372	public function key() {
373		return key($this->_items);
374	}
375
376	/**
377	 * Moves backward to the previous item.  If already at the first item,
378	 * moves to the last one.
379	 *
380	 * @return mixed The current item after moving.
381	 */
382	public function prev() {
383		if (!prev($this->_items)) {
384			end($this->_items);
385		}
386		return current($this->_items);
387	}
388
389	/**
390	 * Move forwards to the next item.
391	 *
392	 * @return The current item after moving.
393	 */
394	public function next() {
395		$this->_valid = (next($this->_items) !== false);
396		return current($this->_items);
397	}
398
399	/**
400	 * Appends an item.
401	 *
402	 * @param mixed $value The item to append.
403	 * @return void
404	 */
405	public function append($value) {
406		is_object($value) ? $this->_items[] =& $value : $this->_items[] = $value;
407	}
408
409	/**
410	 * Counts the items of the object.
411	 *
412	 * @return integer Number of items.
413	 */
414	public function count() {
415		$count = iterator_count($this);
416		$this->rewind();
417		return $count;
418	}
419
420	/**
421	 * Returns the item keys.
422	 *
423	 * @return array The keys of the items.
424	 */
425	public function keys() {
426		return array_keys($this->_items);
427	}
428
429	/**
430	 * Exports a `Collection` instance to an array. Used by `Collection::to()`.
431	 *
432	 * @param mixed $data Either a `Collection` instance, or an array representing a `Collection`'s
433	 *              internal state.
434	 * @return array Returns the value of `$data` as a pure PHP array, recursively converting all
435	 *         sub-objects and other values to their closest array or scalar equivalents.
436	 */
437	protected static function _toArray($data) {
438		$result = array();
439
440		foreach ($data as $key => $item) {
441			switch (true) {
442				case (!is_object($item)):
443					$result[$key] = $item;
444				break;
445				case (method_exists($item, 'to')):
446					$result[$key] = $item->to('array');
447				break;
448				case ($vars = get_object_vars($item)):
449					$result[$key] = $vars;
450				break;
451				case (method_exists($item, '__toString')):
452					$result[$key] = (string) $item;
453				break;
454			}
455		}
456		return $result;
457	}
458}
459
460?>