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