1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* A list object that wraps around an array of objects or arrays. |
4
|
|
|
* |
5
|
|
|
* Note that (like DataLists), the implementations of the methods from SS_Filterable, SS_Sortable and |
6
|
|
|
* SS_Limitable return a new instance of ArrayList, rather than modifying the existing instance. |
7
|
|
|
* |
8
|
|
|
* For easy reference, methods that operate in this way are: |
9
|
|
|
* |
10
|
|
|
* - limit |
11
|
|
|
* - reverse |
12
|
|
|
* - sort |
13
|
|
|
* - filter |
14
|
|
|
* - exclude |
15
|
|
|
* |
16
|
|
|
* @package framework |
17
|
|
|
* @subpackage model |
18
|
|
|
*/ |
19
|
|
|
class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sortable, SS_Limitable { |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Holds the items in the list |
23
|
|
|
* |
24
|
|
|
* @var array |
25
|
|
|
*/ |
26
|
|
|
protected $items = array(); |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* |
30
|
|
|
* @param array $items - an initial array to fill this object with |
31
|
|
|
*/ |
32
|
|
|
public function __construct(array $items = array()) { |
33
|
|
|
$this->items = array_values($items); |
34
|
|
|
parent::__construct(); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Return the class of items in this list, by looking at the first item inside it. |
39
|
|
|
*/ |
40
|
|
|
public function dataClass() { |
41
|
|
|
if(count($this->items) > 0) return get_class($this->items[0]); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Return the number of items in this list |
46
|
|
|
* |
47
|
|
|
* @return int |
48
|
|
|
*/ |
49
|
|
|
public function count() { |
50
|
|
|
return count($this->items); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Returns true if this list has items |
55
|
|
|
* |
56
|
|
|
* @return bool |
57
|
|
|
*/ |
58
|
|
|
public function exists() { |
59
|
|
|
return !empty($this->items); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Returns an Iterator for this ArrayList. |
64
|
|
|
* This function allows you to use ArrayList in foreach loops |
65
|
|
|
* |
66
|
|
|
* @return ArrayIterator |
67
|
|
|
*/ |
68
|
|
|
public function getIterator() { |
69
|
|
|
foreach($this->items as $i => $item) { |
70
|
|
|
if(is_array($item)) $this->items[$i] = new ArrayData($item); |
71
|
|
|
} |
72
|
|
|
return new ArrayIterator($this->items); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Return an array of the actual items that this ArrayList contains. |
77
|
|
|
* |
78
|
|
|
* @return array |
79
|
|
|
*/ |
80
|
|
|
public function toArray() { |
81
|
|
|
return $this->items; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Walks the list using the specified callback |
86
|
|
|
* |
87
|
|
|
* @param callable $callback |
88
|
|
|
* @return DataList |
89
|
|
|
*/ |
90
|
|
|
public function each($callback) { |
91
|
|
|
foreach($this as $item) { |
92
|
|
|
$callback($item); |
93
|
|
|
} |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
public function debug() { |
97
|
|
|
$val = "<h2>" . $this->class . "</h2><ul>"; |
98
|
|
|
foreach($this->toNestedArray() as $item) { |
99
|
|
|
$val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>"; |
100
|
|
|
} |
101
|
|
|
$val .= "</ul>"; |
102
|
|
|
return $val; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Return this list as an array and every object it as an sub array as well |
107
|
|
|
* |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
|
|
public function toNestedArray() { |
111
|
|
|
$result = array(); |
112
|
|
|
|
113
|
|
|
foreach ($this->items as $item) { |
114
|
|
|
if (is_object($item)) { |
115
|
|
|
if (method_exists($item, 'toMap')) { |
116
|
|
|
$result[] = $item->toMap(); |
117
|
|
|
} else { |
118
|
|
|
$result[] = (array) $item; |
119
|
|
|
} |
120
|
|
|
} else { |
121
|
|
|
$result[] = $item; |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
return $result; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get a sub-range of this dataobjectset as an array |
130
|
|
|
* |
131
|
|
|
* @param int $offset |
132
|
|
|
* @param int $length |
133
|
|
|
* @return ArrayList |
134
|
|
|
*/ |
135
|
|
|
public function limit($length, $offset = 0) { |
136
|
|
|
if(!$length) { |
137
|
|
|
$length = count($this->items); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$list = clone $this; |
141
|
|
|
$list->items = array_slice($this->items, $offset, $length); |
142
|
|
|
|
143
|
|
|
return $list; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Add this $item into this list |
148
|
|
|
* |
149
|
|
|
* @param mixed $item |
150
|
|
|
*/ |
151
|
|
|
public function add($item) { |
152
|
|
|
$this->push($item); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Remove this item from this list |
157
|
|
|
* |
158
|
|
|
* @param mixed $item |
159
|
|
|
*/ |
160
|
|
|
public function remove($item) { |
161
|
|
|
$renumberKeys = false; |
162
|
|
|
foreach ($this->items as $key => $value) { |
163
|
|
|
if ($item === $value) { |
164
|
|
|
$renumberKeys = true; |
165
|
|
|
unset($this->items[$key]); |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
if($renumberKeys) $this->items = array_values($this->items); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Replaces an item in this list with another item. |
173
|
|
|
* |
174
|
|
|
* @param array|object $item |
175
|
|
|
* @param array|object $with |
176
|
|
|
* @return void; |
|
|
|
|
177
|
|
|
*/ |
178
|
|
|
public function replace($item, $with) { |
179
|
|
|
foreach ($this->items as $key => $candidate) { |
180
|
|
|
if ($candidate === $item) { |
181
|
|
|
$this->items[$key] = $with; |
182
|
|
|
return; |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Merges with another array or list by pushing all the items in it onto the |
189
|
|
|
* end of this list. |
190
|
|
|
* |
191
|
|
|
* @param array|object $with |
192
|
|
|
*/ |
193
|
|
|
public function merge($with) { |
194
|
|
|
foreach ($with as $item) $this->push($item); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Removes items from this list which have a duplicate value for a certain |
199
|
|
|
* field. This is especially useful when combining lists. |
200
|
|
|
* |
201
|
|
|
* @param string $field |
202
|
|
|
*/ |
203
|
|
|
public function removeDuplicates($field = 'ID') { |
204
|
|
|
$seen = array(); |
205
|
|
|
$renumberKeys = false; |
206
|
|
|
|
207
|
|
|
foreach ($this->items as $key => $item) { |
208
|
|
|
$value = $this->extractValue($item, $field); |
209
|
|
|
|
210
|
|
|
if (array_key_exists($value, $seen)) { |
211
|
|
|
$renumberKeys = true; |
212
|
|
|
unset($this->items[$key]); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$seen[$value] = true; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
if($renumberKeys) $this->items = array_values($this->items); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Pushes an item onto the end of this list. |
223
|
|
|
* |
224
|
|
|
* @param array|object $item |
225
|
|
|
*/ |
226
|
|
|
public function push($item) { |
227
|
|
|
$this->items[] = $item; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Pops the last element off the end of the list and returns it. |
232
|
|
|
* |
233
|
|
|
* @return array|object |
234
|
|
|
*/ |
235
|
|
|
public function pop() { |
236
|
|
|
return array_pop($this->items); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Add an item onto the beginning of the list. |
241
|
|
|
* |
242
|
|
|
* @param array|object $item |
243
|
|
|
*/ |
244
|
|
|
public function unshift($item) { |
245
|
|
|
array_unshift($this->items, $item); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Shifts the item off the beginning of the list and returns it. |
250
|
|
|
* |
251
|
|
|
* @return array|object |
252
|
|
|
*/ |
253
|
|
|
public function shift() { |
254
|
|
|
return array_shift($this->items); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Returns the first item in the list |
259
|
|
|
* |
260
|
|
|
* @return mixed |
261
|
|
|
*/ |
262
|
|
|
public function first() { |
263
|
|
|
return reset($this->items); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Returns the last item in the list |
268
|
|
|
* |
269
|
|
|
* @return mixed |
270
|
|
|
*/ |
271
|
|
|
public function last() { |
272
|
|
|
return end($this->items); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Returns a map of this list |
277
|
|
|
* |
278
|
|
|
* @param string $keyfield The 'key' field of the result array |
279
|
|
|
* @param string $titlefield The value field of the result array |
280
|
|
|
* @return SS_Map |
281
|
|
|
*/ |
282
|
|
|
public function map($keyfield = 'ID', $titlefield = 'Title') { |
283
|
|
|
$list = clone $this; |
284
|
|
|
return new SS_Map($list, $keyfield, $titlefield); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Find the first item of this list where the given key = value |
289
|
|
|
* |
290
|
|
|
* @param string $key |
291
|
|
|
* @param string $value |
292
|
|
|
* @return mixed |
293
|
|
|
*/ |
294
|
|
|
public function find($key, $value) { |
295
|
|
|
foreach ($this->items as $item) { |
296
|
|
|
if ($this->extractValue($item, $key) == $value) { |
297
|
|
|
return $item; |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Returns an array of a single field value for all items in the list. |
304
|
|
|
* |
305
|
|
|
* @param string $colName |
306
|
|
|
* @return array |
307
|
|
|
*/ |
308
|
|
|
public function column($colName = 'ID') { |
309
|
|
|
$result = array(); |
310
|
|
|
|
311
|
|
|
foreach ($this->items as $item) { |
312
|
|
|
$result[] = $this->extractValue($item, $colName); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return $result; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* You can always sort a ArrayList |
320
|
|
|
* |
321
|
|
|
* @param string $by |
322
|
|
|
* @return bool |
323
|
|
|
*/ |
324
|
|
|
public function canSortBy($by) { |
325
|
|
|
return true; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Reverses an {@link ArrayList} |
330
|
|
|
* |
331
|
|
|
* @return ArrayList |
332
|
|
|
*/ |
333
|
|
|
public function reverse() { |
334
|
|
|
$list = clone $this; |
335
|
|
|
$list->items = array_reverse($this->items); |
336
|
|
|
|
337
|
|
|
return $list; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Parses a specified column into a sort field and direction |
342
|
|
|
* |
343
|
|
|
* @param type $column String to parse containing the column name |
344
|
|
|
* @param type $direction Optional Additional argument which may contain the direction |
345
|
|
|
* @return array Sort specification in the form array("Column", SORT_ASC). |
346
|
|
|
*/ |
347
|
|
|
protected function parseSortColumn($column, $direction = null) { |
348
|
|
|
// Substitute the direction for the column if column is a numeric index |
349
|
|
|
if($direction && (empty($column) || is_numeric($column))) { |
350
|
|
|
$column = $direction; |
351
|
|
|
$direction = null; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
// Parse column specification, considering possible ansi sql quoting |
355
|
|
|
if(preg_match('/^"?(?<column>[^"\s]+)"?(\s+(?<direction>((asc)|(desc))(ending)?))?$/i', $column, $match)) { |
356
|
|
|
$column = $match['column']; |
357
|
|
|
if(empty($direction) && !empty($match['direction'])) { |
358
|
|
|
$direction = $match['direction']; |
359
|
|
|
} |
360
|
|
|
} else { |
361
|
|
|
throw new InvalidArgumentException("Invalid sort() column"); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
// Parse sort direction specification |
365
|
|
|
if(empty($direction) || preg_match('/^asc(ending)?$/i', $direction)) { |
366
|
|
|
$direction = SORT_ASC; |
367
|
|
|
} elseif(preg_match('/^desc(ending)?$/i', $direction)) { |
368
|
|
|
$direction = SORT_DESC; |
369
|
|
|
} else { |
370
|
|
|
throw new InvalidArgumentException("Invalid sort() direction"); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
return array($column, $direction); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Sorts this list by one or more fields. You can either pass in a single |
378
|
|
|
* field name and direction, or a map of field names to sort directions. |
379
|
|
|
* |
380
|
|
|
* Note that columns may be double quoted as per ANSI sql standard |
381
|
|
|
* |
382
|
|
|
* @return DataList |
383
|
|
|
* @see SS_List::sort() |
384
|
|
|
* @example $list->sort('Name'); // default ASC sorting |
385
|
|
|
* @example $list->sort('Name DESC'); // DESC sorting |
386
|
|
|
* @example $list->sort('Name', 'ASC'); |
387
|
|
|
* @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC')); |
388
|
|
|
*/ |
389
|
|
|
public function sort() { |
390
|
|
|
$args = func_get_args(); |
391
|
|
|
|
392
|
|
|
if(count($args)==0){ |
393
|
|
|
return $this; |
394
|
|
|
} |
395
|
|
|
if(count($args)>2){ |
396
|
|
|
throw new InvalidArgumentException('This method takes zero, one or two arguments'); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
// One argument and it's a string |
400
|
|
|
if(count($args)==1 && is_string($args[0])){ |
401
|
|
|
list($column, $direction) = $this->parseSortColumn($args[0]); |
402
|
|
|
$columnsToSort[$column] = $direction; |
|
|
|
|
403
|
|
|
|
404
|
|
|
} else if(count($args)==2) { |
405
|
|
|
list($column, $direction) = $this->parseSortColumn($args[0], $args[1]); |
406
|
|
|
$columnsToSort[$column] = $direction; |
|
|
|
|
407
|
|
|
|
408
|
|
|
} else if(is_array($args[0])) { |
409
|
|
|
foreach($args[0] as $key => $value) { |
410
|
|
|
list($column, $direction) = $this->parseSortColumn($key, $value); |
411
|
|
|
$columnsToSort[$column] = $direction; |
|
|
|
|
412
|
|
|
} |
413
|
|
|
} else { |
414
|
|
|
throw new InvalidArgumentException("Bad arguments passed to sort()"); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
// Store the original keys of the items as a sort fallback, so we can preserve the original order in the event |
418
|
|
|
// that array_multisort is unable to work out a sort order for them. This also prevents array_multisort trying |
419
|
|
|
// to inspect object properties which can result in errors with circular dependencies |
420
|
|
|
$originalKeys = array_keys($this->items); |
421
|
|
|
|
422
|
|
|
// This the main sorting algorithm that supports infinite sorting params |
423
|
|
|
$multisortArgs = array(); |
424
|
|
|
$values = array(); |
425
|
|
|
foreach($columnsToSort as $column => $direction) { |
|
|
|
|
426
|
|
|
// The reason these are added to columns is of the references, otherwise when the foreach |
427
|
|
|
// is done, all $values and $direction look the same |
428
|
|
|
$values[$column] = array(); |
429
|
|
|
$sortDirection[$column] = $direction; |
|
|
|
|
430
|
|
|
// We need to subtract every value into a temporary array for sorting |
431
|
|
|
foreach($this->items as $index => $item) { |
432
|
|
|
$values[$column][] = $this->extractValue($item, $column); |
433
|
|
|
} |
434
|
|
|
// PHP 5.3 requires below arguments to be reference when using array_multisort together |
435
|
|
|
// with call_user_func_array |
436
|
|
|
// First argument is the 'value' array to be sorted |
437
|
|
|
$multisortArgs[] = &$values[$column]; |
438
|
|
|
// First argument is the direction to be sorted, |
439
|
|
|
$multisortArgs[] = &$sortDirection[$column]; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
$multisortArgs[] = &$originalKeys; |
443
|
|
|
|
444
|
|
|
$list = clone $this; |
445
|
|
|
// As the last argument we pass in a reference to the items that all the sorting will be applied upon |
446
|
|
|
$multisortArgs[] = &$list->items; |
447
|
|
|
call_user_func_array('array_multisort', $multisortArgs); |
448
|
|
|
return $list; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* Returns true if the given column can be used to filter the records. |
453
|
|
|
* |
454
|
|
|
* It works by checking the fields available in the first record of the list. |
455
|
|
|
*/ |
456
|
|
|
public function canFilterBy($by) { |
457
|
|
|
$firstRecord = $this->first(); |
458
|
|
|
|
459
|
|
|
if ($firstRecord === false) { |
460
|
|
|
return false; |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
return array_key_exists($by, $firstRecord); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Filter the list to include items with these charactaristics |
468
|
|
|
* |
469
|
|
|
* @return ArrayList |
470
|
|
|
* @see SS_List::filter() |
471
|
|
|
* @example $list->filter('Name', 'bob'); // only bob in the list |
472
|
|
|
* @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list |
473
|
|
|
* @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the Age 21 in list |
474
|
|
|
* @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43 |
475
|
|
|
* @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); |
476
|
|
|
* // aziz with the age 21 or 43 and bob with the Age 21 or 43 |
477
|
|
|
*/ |
478
|
|
|
public function filter() { |
479
|
|
|
|
480
|
|
|
$keepUs = call_user_func_array(array($this, 'normaliseFilterArgs'), func_get_args()); |
481
|
|
|
|
482
|
|
|
$itemsToKeep = array(); |
483
|
|
|
foreach($this->items as $item){ |
484
|
|
|
$keepItem = true; |
485
|
|
|
foreach ($keepUs as $column => $value) { |
486
|
|
|
if ((is_array($value) && !in_array($this->extractValue($item, $column), $value)) |
487
|
|
|
|| (!is_array($value) && $this->extractValue($item, $column) != $value) |
488
|
|
|
) { |
489
|
|
|
$keepItem = false; |
490
|
|
|
break; |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
if($keepItem) { |
494
|
|
|
$itemsToKeep[] = $item; |
495
|
|
|
} |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
$list = clone $this; |
499
|
|
|
$list->items = $itemsToKeep; |
500
|
|
|
return $list; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Return a copy of this list which contains items matching any of these charactaristics. |
505
|
|
|
* |
506
|
|
|
* @example // only bob in the list |
507
|
|
|
* $list = $list->filterAny('Name', 'bob'); |
508
|
|
|
* @example // azis or bob in the list |
509
|
|
|
* $list = $list->filterAny('Name', array('aziz', 'bob'); |
510
|
|
|
* @example // bob or anyone aged 21 in the list |
511
|
|
|
* $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21)); |
512
|
|
|
* @example // bob or anyone aged 21 or 43 in the list |
513
|
|
|
* $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43))); |
514
|
|
|
* @example // all bobs, phils or anyone aged 21 or 43 in the list |
515
|
|
|
* $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); |
516
|
|
|
* |
517
|
|
|
* @param string|array See {@link filter()} |
518
|
|
|
* @return DataList |
519
|
|
|
*/ |
520
|
|
|
public function filterAny() { |
521
|
|
|
$keepUs = call_user_func_array(array($this, 'normaliseFilterArgs'), func_get_args()); |
522
|
|
|
|
523
|
|
|
$itemsToKeep = array(); |
524
|
|
|
|
525
|
|
|
foreach ($this->items as $item) { |
526
|
|
|
foreach ($keepUs as $column => $value) { |
527
|
|
|
$extractedValue = $this->extractValue($item, $column); |
528
|
|
|
$matches = is_array($value) ? in_array($extractedValue, $value) : $extractedValue == $value; |
529
|
|
|
if ($matches) { |
530
|
|
|
$itemsToKeep[] = $item; |
531
|
|
|
break; |
532
|
|
|
} |
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
$list = clone $this; |
537
|
|
|
$list->items = array_unique($itemsToKeep, SORT_REGULAR); |
538
|
|
|
return $list; |
539
|
|
|
|
540
|
|
|
} |
541
|
|
|
|
542
|
|
|
/** |
543
|
|
|
* Take the "standard" arguments that the filter/exclude functions take and return a single array with |
544
|
|
|
* 'colum' => 'value' |
545
|
|
|
* |
546
|
|
|
* @param $column array|string The column name to filter OR an assosicative array of column => value |
547
|
|
|
* @param $value array|string|null The values to filter the $column against |
548
|
|
|
* |
549
|
|
|
* @return array The normalised keyed array |
550
|
|
|
*/ |
551
|
|
|
protected function normaliseFilterArgs($column, $value = null) { |
|
|
|
|
552
|
|
|
if(count(func_get_args())>2){ |
553
|
|
|
throw new InvalidArgumentException('filter takes one array or two arguments'); |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
if(count(func_get_args()) == 1 && !is_array(func_get_arg(0))){ |
557
|
|
|
throw new InvalidArgumentException('filter takes one array or two arguments'); |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
$keepUs = array(); |
561
|
|
|
if(count(func_get_args())==2){ |
562
|
|
|
$keepUs[func_get_arg(0)] = func_get_arg(1); |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
if(count(func_get_args())==1 && is_array(func_get_arg(0))){ |
566
|
|
|
foreach(func_get_arg(0) as $column => $value) { |
567
|
|
|
$keepUs[$column] = $value; |
568
|
|
|
} |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
return $keepUs; |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* Filter this list to only contain the given Primary IDs |
576
|
|
|
* |
577
|
|
|
* @param array $ids Array of integers, will be automatically cast/escaped. |
578
|
|
|
* @return ArrayList |
579
|
|
|
*/ |
580
|
|
|
public function byIDs($ids) { |
581
|
|
|
$ids = array_map('intval', $ids); // sanitize |
582
|
|
|
return $this->filter('ID', $ids); |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
public function byID($id) { |
586
|
|
|
$firstElement = $this->filter("ID", $id)->first(); |
587
|
|
|
|
588
|
|
|
if ($firstElement === false) { |
589
|
|
|
return null; |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
return $firstElement; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* @see SS_Filterable::filterByCallback() |
597
|
|
|
* |
598
|
|
|
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; }) |
599
|
|
|
* @param callable $callback |
600
|
|
|
* @return ArrayList |
601
|
|
|
*/ |
602
|
|
|
public function filterByCallback($callback) { |
603
|
|
|
if(!is_callable($callback)) { |
604
|
|
|
throw new LogicException(sprintf( |
605
|
|
|
"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given", |
606
|
|
|
gettype($callback) |
607
|
|
|
)); |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
$output = static::create(); |
611
|
|
|
|
612
|
|
|
foreach($this as $item) { |
613
|
|
|
if(call_user_func($callback, $item, $this)) $output->push($item); |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
return $output; |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* Exclude the list to not contain items with these charactaristics |
621
|
|
|
* |
622
|
|
|
* @return ArrayList |
623
|
|
|
* @see SS_List::exclude() |
624
|
|
|
* @example $list->exclude('Name', 'bob'); // exclude bob from list |
625
|
|
|
* @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list |
626
|
|
|
* @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 |
627
|
|
|
* @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 |
628
|
|
|
* @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); |
629
|
|
|
* // bob age 21 or 43, phil age 21 or 43 would be excluded |
630
|
|
|
*/ |
631
|
|
|
public function exclude() { |
632
|
|
|
|
633
|
|
|
$removeUs = call_user_func_array(array($this, 'normaliseFilterArgs'), func_get_args()); |
634
|
|
|
|
635
|
|
|
$hitsRequiredToRemove = count($removeUs); |
636
|
|
|
$matches = array(); |
637
|
|
|
foreach($removeUs as $column => $excludeValue) { |
638
|
|
|
foreach($this->items as $key => $item){ |
639
|
|
|
if(!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) { |
640
|
|
|
$matches[$key]=isset($matches[$key])?$matches[$key]+1:1; |
641
|
|
|
} elseif(is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue)) { |
642
|
|
|
$matches[$key]=isset($matches[$key])?$matches[$key]+1:1; |
643
|
|
|
} |
644
|
|
|
} |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
$keysToRemove = array_keys($matches,$hitsRequiredToRemove); |
648
|
|
|
|
649
|
|
|
$itemsToKeep = array(); |
650
|
|
|
foreach($this->items as $key => $value) { |
651
|
|
|
if(!in_array($key, $keysToRemove)) { |
652
|
|
|
$itemsToKeep[] = $value; |
653
|
|
|
} |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
$list = clone $this; |
657
|
|
|
$list->items = $itemsToKeep; |
658
|
|
|
return $list; |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
protected function shouldExclude($item, $args) { |
|
|
|
|
662
|
|
|
|
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
|
666
|
|
|
/** |
667
|
|
|
* Returns whether an item with $key exists |
668
|
|
|
* |
669
|
|
|
* @param mixed $key |
|
|
|
|
670
|
|
|
* @return bool |
671
|
|
|
*/ |
672
|
|
|
public function offsetExists($offset) { |
673
|
|
|
return array_key_exists($offset, $this->items); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* Returns item stored in list with index $key |
678
|
|
|
* |
679
|
|
|
* @param mixed $key |
|
|
|
|
680
|
|
|
* @return DataObject |
681
|
|
|
*/ |
682
|
|
|
public function offsetGet($offset) { |
683
|
|
|
if ($this->offsetExists($offset)) return $this->items[$offset]; |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
/** |
687
|
|
|
* Set an item with the key in $key |
688
|
|
|
* |
689
|
|
|
* @param mixed $key |
|
|
|
|
690
|
|
|
* @param mixed $value |
691
|
|
|
*/ |
692
|
|
|
public function offsetSet($offset, $value) { |
693
|
|
|
if($offset == null) { |
694
|
|
|
$this->items[] = $value; |
695
|
|
|
} else { |
696
|
|
|
$this->items[$offset] = $value; |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
/** |
701
|
|
|
* Unset an item with the key in $key |
702
|
|
|
* |
703
|
|
|
* @param mixed $key |
|
|
|
|
704
|
|
|
*/ |
705
|
|
|
public function offsetUnset($offset) { |
706
|
|
|
unset($this->items[$offset]); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
/** |
710
|
|
|
* Extracts a value from an item in the list, where the item is either an |
711
|
|
|
* object or array. |
712
|
|
|
* |
713
|
|
|
* @param array|object $item |
714
|
|
|
* @param string $key |
715
|
|
|
* @return mixed |
716
|
|
|
*/ |
717
|
|
|
protected function extractValue($item, $key) { |
718
|
|
|
if (is_object($item)) { |
719
|
|
|
if(method_exists($item, 'hasMethod') && $item->hasMethod($key)) { |
720
|
|
|
return $item->{$key}(); |
721
|
|
|
} |
722
|
|
|
return $item->{$key}; |
723
|
|
|
} else { |
724
|
|
|
if (array_key_exists($key, $item)) { |
725
|
|
|
return $item[$key]; |
726
|
|
|
} |
727
|
|
|
} |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
} |
731
|
|
|
|
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.