collection.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. /**
  3. * Collection functions, mainly used to manipulate lists of PHPActiveRecord models.
  4. *
  5. * @author Taddeus Kroes
  6. * @version 1.0
  7. * @date 13-07-2012
  8. */
  9. namespace WebBasics;
  10. require_once 'base.php';
  11. /**
  12. * The Collection class contains a number of functions sort, index and manipulate
  13. * an array of items.
  14. *
  15. * Example 1: Index a list based on its values and extract a single attribute.
  16. * <code>
  17. * class Book extends PHPActiveRecord\Model {
  18. * static $attr_accessible = array('id', 'name');
  19. * }
  20. *
  21. * // Index all book names by their id.
  22. * $books = Book::all(); // Find a list of books
  23. * $collection = new Collection($books); // Put the list in a collection
  24. * $indexed = $collection->index_by('id'); // Create indexes for all books
  25. * $names = $indexed->get_attribute('name'); // Get the values of a single attribute
  26. * // $names now contains something like array(1 => 'Some book', 2 => 'Another book')
  27. *
  28. * // Same as above:
  29. * $names = Collection::create(Book::all())->index_by('id')->get_attribute('name');
  30. * </code>
  31. *
  32. * Example 2: Execute a method for each item in a list.
  33. * <code>
  34. * // Delete all books
  35. * Collection::create(Book::all())->map_method('delete');
  36. * </code>
  37. *
  38. * @package WebBasics
  39. * @todo Finish unit tests
  40. */
  41. class Collection extends Base {
  42. /**
  43. * The set of items item that is being manipulated, as an associoative array.
  44. *
  45. * @var array
  46. */
  47. private $items;
  48. /**
  49. * Create a new Collection object.
  50. *
  51. * @param array $items Initial item set (optional).
  52. */
  53. function __construct(array $items=array()) {
  54. $this->items = $items;
  55. }
  56. /**
  57. * Add an item to the collection.
  58. *
  59. * @param mixed $item The item to add.
  60. */
  61. function add($item) {
  62. $this->items[] = $item;
  63. }
  64. /**
  65. * Insert an item at a specific index in the collection.
  66. *
  67. * If the index is numeric, all existing numeric indices from that
  68. * index will be shifted one position to the right.
  69. *
  70. * @param mixed $item The item to insert.
  71. * @param int|string $index The index at which to insert the item.
  72. * @throws \InvalidArgumentException If the added index already exists, and is non-numeric.
  73. */
  74. function insert($item, $index) {
  75. if( isset($this->items[$index]) ) {
  76. if( !is_int($index) )
  77. throw new \InvalidArgumentException(sprintf('Index "%s" already exists in this collection.', $index));
  78. for( $i = count($this->items) - 1; $i >= $index; $i--)
  79. $this->items[$i + 1] = $this->items[$i];
  80. }
  81. $this->items[$index] = $item;
  82. }
  83. /**
  84. * Get all items in the collection as an array.
  85. *
  86. * @return array The items in the collection.
  87. */
  88. function all() {
  89. return $this->items;
  90. }
  91. /**
  92. * Get the first item in the collection.
  93. *
  94. * @return mixed
  95. * @throws \OutOfBoundsException if the collection is empty.
  96. */
  97. function first() {
  98. if( !$this->count() )
  99. throw new \OutOfBoundsException(sprintf('Cannot get first item: collection is empty.'));
  100. return $this->items[0];
  101. }
  102. /**
  103. * Get the last item in the collection.
  104. *
  105. * @return mixed
  106. * @throws \OutOfBoundsException if the collection is empty.
  107. */
  108. function last() {
  109. if( !$this->count() )
  110. throw new \OutOfBoundsException(sprintf('Cannot get last item: collection is empty.'));
  111. return end($this->items);
  112. }
  113. /**
  114. * Get the number of items in the collection.
  115. *
  116. * @return int The number of items in the collection.
  117. */
  118. function count() {
  119. return count($this->items);
  120. }
  121. /**
  122. * Check if an item with the given index exists.
  123. *
  124. * @param int|string $index The index to check existance of.
  125. * @return bool Whether the index exists.
  126. */
  127. function index_exists($index) {
  128. return isset($this->items[$index]);
  129. }
  130. /**
  131. * Get an item from the collection by its index.
  132. *
  133. * @param int|string $index The index to the item to get.
  134. * @return mixed The value corresponding to the specified index.
  135. */
  136. function get($index) {
  137. return $this->items[$index];
  138. }
  139. /**
  140. * Delete an item from the collection by its index.
  141. *
  142. * @param int|string $index The index to the item to delete.
  143. */
  144. function delete_index($index) {
  145. unset($this->items[$index]);
  146. }
  147. /**
  148. * Delete an item from the collection.
  149. *
  150. * @param mixed $item The item to delete.
  151. */
  152. function delete($item) {
  153. $this->delete_index(array_search($item, $this->items));
  154. }
  155. /**
  156. * Create a new item set with the same class name as this object.
  157. *
  158. * @param array $items The new items to create a set with.
  159. * @param bool $clone If TRUE, the item set will overwrite the current
  160. * object's item set and not create a new object.
  161. * @return Collection A collection with the new item set.
  162. */
  163. private function set_items(array $items, $clone=true) {
  164. if( $clone )
  165. return new self($items);
  166. $this->items = $items;
  167. return $this;
  168. }
  169. /**
  170. * Remove duplicates from the current item set.
  171. *
  172. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  173. * @return Collection A collection without duplicates.
  174. */
  175. function uniques($clone=false) {
  176. return $this->set_items(array_values(array_unique($this->items)), $clone);
  177. }
  178. /**
  179. * Filter items from the collection.
  180. *
  181. * @param callable $callback Function that receives an item from the collection and
  182. * returns TRUE if the item should be present in the
  183. * resulting collection.
  184. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  185. * @return Collection A collection with the filtered set of items.
  186. */
  187. function filter($callback, $clone=true) {
  188. return $this->set_items(array_values(array_filter($this->items, $callback)), $clone);
  189. }
  190. /**
  191. * Find a subset of items in the collection using property value conditions.
  192. *
  193. * The conditions are specified as an associative array of property names
  194. * pointing to values. Only items whose properties have these values will
  195. * appear in the resulting collection. The items in the collection have to
  196. * be objects or associative arrays, or an error will occur.
  197. *
  198. * @param array $conditions The conditions that items in the subset should meet.
  199. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  200. * @throws \UnexpectedValueException If a non-object and non-array value is encountered.
  201. * @return Collection
  202. */
  203. function find(array $conditions, $clone=true) {
  204. return $this->filter(function($item) use ($conditions) {
  205. if( is_object($item) ) {
  206. // Object, match property values
  207. foreach( $conditions as $property => $value )
  208. if( $item->{$property} != $value )
  209. return false;
  210. } elseif( is_array($item) ) {
  211. // Array item, match array values
  212. foreach( $conditions as $property => $value )
  213. if( $item[$property] != $value )
  214. return false;
  215. } else {
  216. // Other, incompatible type -> throw exception
  217. throw new \UnexpectedValueException(
  218. sprintf('Collection::find encountered a non-object and non-array item "%s".', $item)
  219. );
  220. }
  221. return true;
  222. }, $clone);
  223. }
  224. /**
  225. * Get an attribute value for each of the items in the collection.
  226. *
  227. * The items are assumed to be objects with the specified attribute.
  228. *
  229. * @param string $attribute The name of the attribute to get the value of.
  230. * @return array The original item keys, pointing to single attribute values.
  231. */
  232. function get_attribute($attribute) {
  233. return array_map(function($item) use ($attribute) {
  234. return $item->{$attribute};
  235. }, $this->items);
  236. }
  237. /**
  238. * Use an attribute of each item in the collection as an index to that item.
  239. *
  240. * The items are assumed to be objects with the specified index attribute.
  241. *
  242. * @param string $attribute The name of the attribute to use as index value.
  243. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  244. * @return Collection A collection object with the values of the attribute used as indices.
  245. */
  246. function index_by($attribute, $clone=true) {
  247. $indexed = array();
  248. foreach( $this->items as $item )
  249. $indexed[$item->$attribute] = $item;
  250. return $this->set_items($indexed, $clone);
  251. }
  252. /**
  253. * Execute a callback for each of the items in the collection.
  254. *
  255. * @param callable $callback Function that receives an item from the collection.
  256. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  257. * @return Collection A collection with return values of the callback calls.
  258. */
  259. function map($callback, $clone=true) {
  260. return $this->set_items(array_map($callback, $this->items), $clone);
  261. }
  262. /**
  263. * Execute an object method for each item in the collection.
  264. *
  265. * The items are assumed to be objects with the specified method.
  266. *
  267. * @param string $method_name The name of the method to execute.
  268. * @param array $args Any arguments to pass to the method.
  269. * @param bool $clone Whether to create a new object, or overwrite the current item set.
  270. * @return Collection A collection with return values of the method calls.
  271. */
  272. function map_method($method_name, array $args=array(), $clone=true) {
  273. $items = array();
  274. foreach( $this->items as $item )
  275. $items[] = call_user_func_array(array($item, $method_name), $args);
  276. return $this->set_items($items);
  277. }
  278. }
  279. ?>