collection.php 9.1 KB

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