Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
<?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);
}
}
?>