collection.php 9.1 KB

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