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