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