<?php /** * Collection functions, mainly used to manipulate lists of PHPActiveRecord models. * * @author Taddeus Kroes * @version 1.0 * @date 13-07-2012 */ namespace WebBasics; require_once 'base.php'; /** * The Collection class contains a number of functions sort, index and manipulate * an array of items. * * Example 1: Index a list based on its values and extract a single attribute. * <code> * class Book extends PHPActiveRecord\Model { * static $attr_accessible = array('id', 'name'); * } * * // Index all book names by their id. * $books = Book::all(); // Find a list of books * $collection = new Collection($books); // Put the list in a collection * $indexed = $collection->index_by('id'); // Create indexes for all books * $names = $indexed->get_attribute('name'); // Get the values of a single attribute * // $names now contains something like array(1 => 'Some book', 2 => 'Another book') * * // Same as above: * $names = Collection::create(Book::all())->index_by('id')->get_attribute('name'); * </code> * * Example 2: Execute a method for each item in a list. * <code> * // Delete all books * Collection::create(Book::all())->map_method('delete'); * </code> * * @package WebBasics * @todo Finish unit tests */ class Collection extends Base { /** * The set of items item that is being manipulated, as an associoative array. * * @var array */ private $items; /** * Create a new Collection object. * * @param array $items Initial item set (optional). */ function __construct(array $items=array()) { $this->items = $items; } /** * Add an item to the collection. * * @param mixed $item The item to add. */ function add($item) { $this->items[] = $item; } /** * Insert an item at a specific index in the collection. * * If the index is numeric, all existing numeric indices from that * index will be shifted one position to the right. * * @param mixed $item The item to insert. * @param int|string $index The index at which to insert the item. * @throws \InvalidArgumentException If the added index already exists, and is non-numeric. */ function insert($item, $index) { if( isset($this->items[$index]) ) { if( !is_int($index) ) throw new \InvalidArgumentException(sprintf('Index "%s" already exists in this collection.', $index)); for( $i = count($this->items) - 1; $i >= $index; $i--) $this->items[$i + 1] = $this->items[$i]; } $this->items[$index] = $item; } /** * Get all items in the collection as an array. * * @return array The items in the collection. */ function all() { return $this->items; } /** * Get the first item in the collection. * * @return mixed * @throws \OutOfBoundsException if the collection is empty. */ function first() { if( !$this->count() ) throw new \OutOfBoundsException(sprintf('Cannot get first item: collection is empty.')); return $this->items[0]; } /** * Get the last item in the collection. * * @return mixed * @throws \OutOfBoundsException if the collection is empty. */ function last() { if( !$this->count() ) throw new \OutOfBoundsException(sprintf('Cannot get last item: collection is empty.')); return end($this->items); } /** * Get the number of items in the collection. * * @return int The number of items in the collection. */ function count() { return count($this->items); } /** * Check if an item with the given index exists. * * @param int|string $index The index to check existance of. * @return bool Whether the index exists. */ function index_exists($index) { return isset($this->items[$index]); } /** * Get an item from the collection by its index. * * @param int|string $index The index to the item to get. * @return mixed The value corresponding to the specified index. */ function get($index) { return $this->items[$index]; } /** * Delete an item from the collection by its index. * * @param int|string $index The index to the item to delete. */ function delete_index($index) { unset($this->items[$index]); } /** * Delete an item from the collection. * * @param mixed $item The item to delete. */ function delete($item) { $this->delete_index(array_search($item, $this->items)); } /** * Create a new item set with the same class name as this object. * * @param array $items The new items to create a set with. * @param bool $clone If TRUE, the item set will overwrite the current * object's item set and not create a new object. * @return Collection A collection with the new item set. */ private function set_items(array $items, $clone=true) { if( $clone ) return new self($items); $this->items = $items; return $this; } /** * Remove duplicates from the current item set. * * @param bool $clone Whether to create a new object, or overwrite the current item set. * @return Collection A collection without duplicates. */ function uniques($clone=false) { return $this->set_items(array_values(array_unique($this->items)), $clone); } /** * Filter items from the collection. * * @param callable $callback Function that receives an item from the collection and * returns TRUE if the item should be present in the * resulting collection. * @param bool $clone Whether to create a new object, or overwrite the current item set. * @return Collection A collection with the filtered set of items. */ function filter($callback, $clone=true) { return $this->set_items(array_values(array_filter($this->items, $callback)), $clone); } /** * Find a subset of items in the collection using property value conditions. * * The conditions are specified as an associative array of property names * pointing to values. Only items whose properties have these values will * appear in the resulting collection. The items in the collection have to * be objects or associative arrays, or an error will occur. * * @param array $conditions The conditions that items in the subset should meet. * @param bool $clone Whether to create a new object, or overwrite the current item set. * @throws \UnexpectedValueException If a non-object and non-array value is encountered. * @return Collection */ function find(array $conditions, $clone=true) { return $this->filter(function($item) use ($conditions) { if( is_object($item) ) { // Object, match property values foreach( $conditions as $property => $value ) if( $item->{$property} != $value ) return false; } elseif( is_array($item) ) { // Array item, match array values foreach( $conditions as $property => $value ) if( $item[$property] != $value ) return false; } else { // Other, incompatible type -> throw exception throw new \UnexpectedValueException( sprintf('Collection::find encountered a non-object and non-array item "%s".', $item) ); } return true; }, $clone); } /** * Get an attribute value for each of the items in the collection. * * The items are assumed to be objects with the specified attribute. * * @param string $attribute The name of the attribute to get the value of. * @return array The original item keys, pointing to single attribute values. */ function get_attribute($attribute) { return array_map(function($item) use ($attribute) { return $item->{$attribute}; }, $this->items); } /** * Use an attribute of each item in the collection as an index to that item. * * The items are assumed to be objects with the specified index attribute. * * @param string $attribute The name of the attribute to use as index value. * @param bool $clone Whether to create a new object, or overwrite the current item set. * @return Collection A collection object with the values of the attribute used as indices. */ function index_by($attribute, $clone=true) { $indexed = array(); foreach( $this->items as $item ) $indexed[$item->$attribute] = $item; return $this->set_items($indexed, $clone); } /** * Execute a callback for each of the items in the collection. * * @param callable $callback Function that receives an item from the collection. * @param bool $clone Whether to create a new object, or overwrite the current item set. * @return Collection A collection with return values of the callback calls. */ function map($callback, $clone=true) { return $this->set_items(array_map($callback, $this->items), $clone); } /** * Execute an object method for each item in the collection. * * The items are assumed to be objects with the specified method. * * @param string $method_name The name of the method to execute. * @param array $args Any arguments to pass to the method. * @param bool $clone Whether to create a new object, or overwrite the current item set. * @return Collection A collection with return values of the method calls. */ function map_method($method_name, array $args=array(), $clone=true) { $items = array(); foreach( $this->items as $item ) $items[] = call_user_func_array(array($item, $method_name), $args); return $this->set_items($items); } } ?>