1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
namespace Arrgh; |
4
|
|
|
|
5
|
|
|
use \Closure; |
6
|
|
|
use \Exception; |
7
|
|
|
use \InvalidArgumentException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* A chainable array API or a set of static functions, or both. |
11
|
|
|
* |
12
|
|
|
* Note: arr_* global functions are defined at the end of the file |
13
|
|
|
* |
14
|
|
|
* @method string getString() |
15
|
|
|
* @method void setInteger(integer $integer) |
16
|
|
|
* @method setString(integer $integer) |
17
|
|
|
* @method array collapse(array $input) |
18
|
|
|
* @method bool contains(array $haystack, string $needle, string $key) |
19
|
|
|
* @method array except(array $input, array|string $keys) |
20
|
|
|
* @method array only(array $input, array|string $keys) |
21
|
|
|
* @method array map_assoc(array $input, \Closure $callable) |
22
|
|
|
* @method array sort_by(array $input, string $key) |
23
|
|
|
* @method integer depth(array $input) |
24
|
|
|
* @method array even(array $input) |
25
|
|
|
* @method mixed first(array $input) |
26
|
|
|
* @method array get(array, $input, array|string, $path, bool $collapse) |
27
|
|
|
* @method mixed head(array $input) |
28
|
|
|
* @method bool is_collection(array $input) |
29
|
|
|
* @method mixed last(array $input) |
30
|
|
|
* @method array odd(array $input) |
31
|
|
|
* @method array partition(array $input, \Closure $callable) |
32
|
|
|
* @method array tail(array $input) |
33
|
|
|
*/ |
34
|
|
|
class Arrgh implements \ArrayAccess, \Iterator |
35
|
|
|
{ |
36
|
|
|
const PHP_SORT_DIRECTION_56 = 1; |
37
|
|
|
const PHP_SORT_DIRECTION_7 = -1; |
38
|
|
|
|
39
|
|
|
private $array; |
40
|
|
|
private $array_position; |
41
|
|
|
private $original_array; |
42
|
|
|
private $terminate; |
43
|
|
|
private $keep_once; |
44
|
|
|
private $last_value; |
45
|
|
|
|
46
|
|
|
private static $php_version; |
47
|
|
|
private static $php_sort_direction; |
48
|
|
|
|
49
|
|
|
/* Creates a new arrgh array */ |
50
|
|
|
public function __construct($array = []) |
51
|
|
|
{ |
52
|
|
|
$this->array = $array; |
53
|
|
|
$this->array_position = 0; |
54
|
|
|
if ($array instanceof Arrgh) { |
55
|
|
|
$this->array = $array->toArray(); |
56
|
|
|
} |
57
|
|
|
$this->original_array = $this->array; |
58
|
|
|
$this->terminate = true; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/* Starts object calls */ |
62
|
|
|
public function __call($method, $args) |
63
|
|
|
{ |
64
|
|
|
return self::invoke($method, $args, $this); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/* Returns an array */ |
68
|
|
|
public function toArray() |
69
|
|
|
{ |
70
|
|
|
$array = array_map(function($item) { |
71
|
|
|
if ($item instanceof Arrgh) { |
72
|
|
|
return $item->toArray(); |
73
|
|
|
} |
74
|
|
|
return $item; |
75
|
|
|
}, $this->array); |
76
|
|
|
return $array; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function keep() |
80
|
|
|
{ |
81
|
|
|
return $this->keepChain(true); |
82
|
|
|
} |
83
|
|
|
public function keepOnce() |
84
|
|
|
{ |
85
|
|
|
return $this->keepChain(true, true); |
86
|
|
|
} |
87
|
|
|
public function keepChain($value = true, $keep_once = false) |
88
|
|
|
{ |
89
|
|
|
$this->terminate = !$value; |
90
|
|
|
$this->keep_once = $keep_once; |
91
|
|
|
return $this; |
92
|
|
|
} |
93
|
|
|
public function breakChain() |
94
|
|
|
{ |
95
|
|
|
return $this->keepChain(false); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/* ArrayAccess */ |
99
|
|
|
public function offsetExists($offset) |
100
|
|
|
{ |
101
|
|
|
return isset($this->array[$offset]); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/* ArrayAccess */ |
105
|
|
|
public function offsetGet($offset) |
106
|
|
|
{ |
107
|
|
|
return isset($this->array[$offset]) ? $this->array[$offset] : null; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/* ArrayAccess */ |
111
|
|
|
public function offsetSet($offset, $value) |
112
|
|
|
{ |
113
|
|
|
if (is_null($offset)) { |
114
|
|
|
$this->array[] = $value; |
115
|
|
|
} else { |
116
|
|
|
$this->array[$offset] = $value; |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/* ArrayAccess */ |
121
|
|
|
public function offsetUnset($offset) |
122
|
|
|
{ |
123
|
|
|
unset($this->array[$offset]); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/* Iterator */ |
127
|
|
|
public function current() |
128
|
|
|
{ |
129
|
|
|
$value = $this->array[$this->array_position]; |
130
|
|
|
if (is_array($value)) { |
131
|
|
|
return new Arrgh($value); |
132
|
|
|
} |
133
|
|
|
return $value; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/* Iterator */ |
137
|
|
|
public function key() |
138
|
|
|
{ |
139
|
|
|
return $this->array_position; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/* Iterator */ |
143
|
|
|
public function next() |
144
|
|
|
{ |
145
|
|
|
++$this->array_position; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/* Iterator */ |
149
|
|
|
public function rewind() |
150
|
|
|
{ |
151
|
|
|
$this->array_position = 0; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/* Iterator */ |
155
|
|
|
public function valid() |
156
|
|
|
{ |
157
|
|
|
return isset($this->array[$this->array_position]); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/* Creates a new arr array. Synonym for: chain() */ |
161
|
|
|
public static function arr($array = []) |
162
|
|
|
{ |
163
|
|
|
return self::chain($array); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/* Creates a new arrgh array. Synonym for: arr() */ |
167
|
|
|
public static function chain($array = []) |
168
|
|
|
{ |
169
|
|
|
return new self($array); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/* Starts object calls */ |
173
|
|
|
public static function __callStatic($method, $args) |
174
|
|
|
{ |
175
|
|
|
if ($method[0] === "_") { |
176
|
|
|
$method = substr($method, 1); |
177
|
|
|
$_args = $args; |
178
|
|
|
$first_argument = array_shift($args); |
179
|
|
|
if (is_array($first_argument)) { |
180
|
|
|
return self::chain($first_argument)->$method(...$args); |
181
|
|
|
} |
182
|
|
|
return self::chain()->$method(...$_args); |
183
|
|
|
} |
184
|
|
|
return self::invoke($method, $args); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
public static function allFunctions() |
188
|
|
|
{ |
189
|
|
|
return [ |
190
|
|
|
"_arrgh" => self::$arr_functions, |
191
|
|
|
"_call" => self::$simple_functions, |
192
|
|
|
"_rotateRight" => self::$reverse_functions, |
193
|
|
|
"_swapTwoFirst" => self::$swapped_functions, |
194
|
|
|
"_copy" => self::$mutable_functions, |
195
|
|
|
"_copyMultiple" => self::$mutable_functions_multiple, |
196
|
|
|
"_copyValue" => self::$mutable_value_functions, |
197
|
|
|
]; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
public static function getSortDirection($direction = null) |
201
|
|
|
{ |
202
|
|
|
if (self::$php_version === null) { |
203
|
|
|
self::$php_version = explode(".", phpversion()); |
204
|
|
|
self::$php_sort_direction = self::$php_version[0] >= 7 ? self::PHP_SORT_DIRECTION_7 : self::PHP_SORT_DIRECTION_56; |
205
|
|
|
} |
206
|
|
|
if ($direction === null || $direction === 0) { |
207
|
|
|
return self::$php_sort_direction; |
208
|
|
|
} |
209
|
|
|
return $direction; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/* Wraps a callable with the purpose of fixing bad PHP sort implementations */ |
213
|
|
|
private static function wrapCallable(Closure $callable) |
214
|
|
|
{ |
215
|
|
|
$direction = self::getSortDirection(); |
216
|
|
|
return function($a, $b) use ($direction, $callable) { |
217
|
|
|
$result = $callable($a, $b); |
218
|
|
|
if ($result === 0) return $direction; |
219
|
|
|
return $result; |
220
|
|
|
}; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/* Based on input method finds handler, function and post handler */ |
224
|
|
|
private static function findFunction($method) |
225
|
|
|
{ |
226
|
|
|
$snake = strtolower(preg_replace('/\B([A-Z])/', '_\1', $method)); |
227
|
|
|
$function_name = $snake; |
228
|
|
|
$function_name_prefixed = stripos($method, "array_") === 0 ? $snake : "array_" . $snake; |
229
|
|
|
|
230
|
|
|
$all_function_names = [$function_name, $function_name_prefixed]; |
231
|
|
|
$all_functions = self::allFunctions(); |
232
|
|
|
|
233
|
|
|
$matching_handler = null; |
234
|
|
|
$matching_function = null; |
235
|
|
|
$post_handler = null; |
236
|
|
|
foreach ($all_functions as $handler => $functions) { |
237
|
|
|
foreach ($all_function_names as $function) { |
238
|
|
|
if (in_array($function, $functions)) { |
239
|
|
|
$matching_handler = $handler; |
240
|
|
|
$matching_function = $function; |
241
|
|
|
break 2; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
if ($matching_function === null) { |
247
|
|
|
throw new InvalidArgumentException("Method {$method} doesn't exist"); |
248
|
|
|
} |
249
|
|
|
return [$matching_handler, $matching_function, $post_handler]; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/* Transforms the incoming calls to native calls */ |
253
|
|
|
private static function invoke($method, $args, $object = null) |
254
|
|
|
{ |
255
|
|
|
self::getSortDirection(); |
256
|
|
|
|
257
|
|
|
list($matching_handler, $matching_function, $post_handler) = self::findFunction($method); |
258
|
|
|
|
259
|
|
|
switch ($matching_function) { |
260
|
|
|
case "asort": |
261
|
|
|
self::handleCaseAsort($matching_handler, $matching_function, $post_handler, $args); |
262
|
|
|
break; |
263
|
|
|
case "array_column": |
264
|
|
|
self::handleCaseArrayColumn($matching_handler, $matching_function, $post_handler, $args); |
265
|
|
|
break; |
266
|
|
|
default: |
267
|
|
|
break; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
// If chain unshift array onto argument stack |
271
|
|
|
if ($object && !in_array($matching_function, self::$starters)) { |
272
|
|
|
array_unshift($args, $object->array); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
// If some arrays are Arrghs map to array or if callable, wrap it in |
276
|
|
|
// new callable with info about sort direction. |
277
|
|
|
$args = array_map(function($arg) use ($matching_function) { |
278
|
|
|
if ($arg instanceof Arrgh) { |
279
|
|
|
return $arg->array; |
280
|
|
|
} else if ($arg instanceof Closure) { |
281
|
|
|
if (in_array($matching_function, self::$reverse_result_functions) && self::$php_version[0] < 7) { |
282
|
|
|
return self::wrapCallable($arg); |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
return $arg; |
286
|
|
|
}, $args); |
287
|
|
|
|
288
|
|
|
// Invoke handler |
289
|
|
|
// (issue with strings that has been passed as reference when using |
290
|
|
|
// them as callable, see https://bugs.php.net/bug.php?id=71622) |
|
|
|
|
291
|
|
|
$reldnah_gnihctam = $matching_handler; |
292
|
|
|
$result = self::$reldnah_gnihctam($matching_function, $args, $object); |
293
|
|
|
|
294
|
|
|
// If a post handler is registered let it modify the result |
295
|
|
|
if ($post_handler) { |
296
|
|
|
$result = $post_handler($result); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
if ($object) { |
300
|
|
|
if (in_array($matching_function, self::$terminators)) { |
301
|
|
|
if ($object->terminate) { |
302
|
|
|
if (is_array($result)) { |
303
|
|
|
return new Arrgh($result); |
304
|
|
|
} |
305
|
|
|
return $result; |
306
|
|
|
} |
307
|
|
|
if ($object->keep_once) { |
308
|
|
|
$object->terminate = true; |
309
|
|
|
$object->keep_once = false; |
310
|
|
|
} |
311
|
|
|
$object->last_value = $result; |
312
|
|
|
return $object; |
313
|
|
|
} |
314
|
|
|
$object->array = $result; |
315
|
|
|
return $object; |
316
|
|
|
} |
317
|
|
|
return $result; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/* Handles special case: asort - In PHP5 reverses equals ("arsort" doen't mess up for some reason) */ |
321
|
|
|
private static function handleCaseAsort(&$matching_handler, &$matching_function, &$post_handler, &$args) |
|
|
|
|
322
|
|
|
{ |
323
|
|
|
$matching_function = "uasort"; |
324
|
|
|
array_push($args, function($a, $b) { return strcasecmp($a, $b); }); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/* Handles special case: array_column - Native array_column filters away null values. |
328
|
|
|
* That means you cannot use array_column for multisort since array size no longer matches. |
329
|
|
|
* This version of array_column returns null if the column is missing. */ |
330
|
|
|
private static function handleCaseArrayColumn(&$matching_handler, &$matching_function, &$post_handler, &$args) |
331
|
|
|
{ |
332
|
|
|
$matching_handler = "_rotateRight"; |
333
|
|
|
$matching_function = "array_map"; |
334
|
|
|
$column_array = $args[0]; |
335
|
|
|
$column_key = $args[1]; |
336
|
|
|
if (count($args) === 3) { |
337
|
|
|
$column_id = $args[2]; |
338
|
|
|
$column_ids_new = array_map(function($item) use ($column_id) { |
339
|
|
|
return isset($item[$column_id]) ? $item[$column_id] : null; |
340
|
|
|
}, $column_array); |
341
|
|
|
$post_handler = function($result) use ($column_ids_new) { |
342
|
|
|
return array_combine($column_ids_new, $result); |
343
|
|
|
}; |
344
|
|
|
} |
345
|
|
|
$args = [$column_array]; |
346
|
|
|
array_push($args, function($item) use ($column_key) { |
347
|
|
|
return isset($item[$column_key]) ? $item[$column_key] : null; |
348
|
|
|
}); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/* Calls the native function directly */ |
352
|
|
|
private static function _call($function, $args) |
353
|
|
|
{ |
354
|
|
|
return $function(...$args); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/* Shifts of the first argument (callable) and pushes it to the end */ |
358
|
|
|
private static function _rotateRight($function, $args) |
359
|
|
|
{ |
360
|
|
|
$first_argument = array_pop($args); |
361
|
|
|
array_unshift($args, $first_argument); |
362
|
|
|
return $function(...$args); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/* Swaps the first two args */ |
366
|
|
|
private static function _swapTwoFirst($function, $args) |
367
|
|
|
{ |
368
|
|
|
$first_argument = array_shift($args); |
369
|
|
|
$second_argument = array_shift($args); |
370
|
|
|
array_unshift($args, $first_argument); |
371
|
|
|
array_unshift($args, $second_argument); |
372
|
|
|
return $function(...$args); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/* Makes a copy of the array and returns it after invoking function */ |
376
|
|
|
private static function _copy($function, $args) |
377
|
|
|
{ |
378
|
|
|
$array = array_shift($args); |
379
|
|
|
$function($array, ...$args); |
380
|
|
|
return $array; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/* If multiple arrays are passed as arguments mulitple will be returned. Otherwise _copy is used */ |
384
|
|
|
private static function _copyMultiple($function, $args) |
385
|
|
|
{ |
386
|
|
|
$function(...$args); |
387
|
|
|
$arrays = []; |
388
|
|
|
foreach ($args as $arg) { |
389
|
|
|
if (is_array($arg)) { |
390
|
|
|
$arrays[] = $arg; |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
if (count($arrays) === 1) { |
394
|
|
|
return $arrays[0]; |
395
|
|
|
} |
396
|
|
|
return $arrays; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/* Makes a copy of the array and returns it after invoking function */ |
400
|
|
|
private static function _copyValue($function, $args, $object = null) |
401
|
|
|
{ |
402
|
|
|
$array = array_shift($args); |
403
|
|
|
$result = $function($array, ...$args); |
404
|
|
|
if ($object) { |
405
|
|
|
$object->array = $array; |
406
|
|
|
} |
407
|
|
|
return $result; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
private static function _arrgh($function, $args) |
411
|
|
|
{ |
412
|
|
|
$function = "arr_" . $function; |
413
|
|
|
return self::$function(...$args); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
private static function arr_map_assoc($array, Closure $callable) |
417
|
|
|
{ |
418
|
|
|
$keys = array_keys($array); |
419
|
|
|
return array_combine($keys, array_map($callable, $keys, $array)); |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Sort an array of associative arrays by key. It checks the first two values for type |
424
|
|
|
* either sorts by number or using strcmp. If a key is missing entries are moved to the top |
425
|
|
|
* (or bottom depending on $direction) |
426
|
|
|
*/ |
427
|
|
|
private static function arr_sort_by($array, $key, $direction = "ASC") |
428
|
|
|
{ |
429
|
|
|
$direction_int = strtoupper($direction) === "ASC" ? 1 : -1; |
430
|
|
|
|
431
|
|
|
if ($key instanceof Closure) { |
432
|
|
|
usort($array, self::wrapCallable($key)); |
433
|
|
|
if ($direction_int === -1) { |
434
|
|
|
return array_reverse($array); |
435
|
|
|
} |
436
|
|
|
return $array; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
$column = array_map(function($item) use ($key) { |
440
|
|
|
return isset($item[$key]) ? $item[$key] : null; |
441
|
|
|
}, $array); |
442
|
|
|
array_multisort($column, ($direction_int === 1 ? SORT_ASC : SORT_DESC), $array); |
443
|
|
|
return $array; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
private static function arr_collapse($array) |
447
|
|
|
{ |
448
|
|
|
return array_reduce($array, function($merged, $item) { |
449
|
|
|
if (is_array($item)) { |
450
|
|
|
return array_merge($merged, $item); |
451
|
|
|
} |
452
|
|
|
$merged[] = $item; |
453
|
|
|
return $merged; |
454
|
|
|
}, []); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
private static function arr_contains($array, $search, $key = null) |
458
|
|
|
{ |
459
|
|
|
$haystack = null; |
|
|
|
|
460
|
|
|
if ($key) { |
461
|
|
|
$haystack = array_column($array, $key); |
462
|
|
|
} else { |
463
|
|
|
$haystack = array_reduce($array, function($merged, $item) { |
464
|
|
|
return array_merge($merged, array_values($item)); |
465
|
|
|
}, []); |
466
|
|
|
} |
467
|
|
|
return array_search($search, $haystack) !== false; |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
private static function arr_except($array, $except) |
471
|
|
|
{ |
472
|
|
|
if (is_string($except)) { |
473
|
|
|
$except = [$except]; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
$is_collection = self::arr_is_collection($array); |
477
|
|
|
$array = $is_collection ? $array : [$array]; |
478
|
|
|
|
479
|
|
|
$result = array_map(function($item) use ($except) { |
480
|
|
|
foreach ($except as $key) { |
481
|
|
|
unset($item[$key]); |
482
|
|
|
} |
483
|
|
|
return $item; |
484
|
|
|
}, $array); |
485
|
|
|
|
486
|
|
|
if ($is_collection) { |
487
|
|
|
return $result; |
488
|
|
|
} |
489
|
|
|
return $result[0]; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
private static function arr_only($array, $only) |
493
|
|
|
{ |
494
|
|
|
if (is_string($only)) { |
495
|
|
|
$only = [$only]; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
$is_collection = self::arr_is_collection($array); |
499
|
|
|
$array = $is_collection ? $array : [$array]; |
500
|
|
|
|
501
|
|
|
$result = array_map(function($item) use ($only) { |
502
|
|
|
foreach ($item as $key => $value) { |
503
|
|
|
if (!in_array($key, $only)) { |
504
|
|
|
unset($item[$key]); |
505
|
|
|
} |
506
|
|
|
} |
507
|
|
|
return $item; |
508
|
|
|
}, $array); |
509
|
|
|
|
510
|
|
|
if ($is_collection) { |
511
|
|
|
return $result; |
512
|
|
|
} |
513
|
|
|
return $result[0]; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Get for multi-dimensional arrays |
518
|
|
|
* |
519
|
|
|
* @param array An array to query on |
520
|
|
|
* @param path|array A string representing the path to traverse. |
521
|
|
|
* Optionally pass as [ $path, ...$functions ] if `!$` is used |
522
|
|
|
* @param bool Collapse resulting data-set |
523
|
|
|
* @throws Exception Thrown when a path cannot be reached in case $array does |
524
|
|
|
* not correspond to path type. E.g. collection expected |
525
|
|
|
* but a simple value was encountered. |
526
|
|
|
*/ |
527
|
|
|
private static function arr_get($array, $path, $collapse = false) |
528
|
|
|
{ |
529
|
|
|
$path_string = $path; |
530
|
|
|
if (is_array($path)) { |
531
|
|
|
$path_string = array_shift($path); |
532
|
|
|
} |
533
|
|
|
$path_segments = explode(".", $path_string); |
534
|
|
|
return self::_arr_get_traverse($array, $path_segments, $collapse, /* functions */ $path); |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/* arr_get: Traverses path to get value */ |
538
|
|
|
private static function _arr_get_traverse($data, $path, $collapse = false, $functions = []) |
539
|
|
|
{ |
540
|
|
|
$next_key = array_shift($path); |
541
|
|
|
$plug_index = is_numeric($next_key) ? (int) $next_key : null; |
542
|
|
|
$is_collection = self::isCollection($data); |
543
|
|
|
$next_node = null; |
|
|
|
|
544
|
|
|
|
545
|
|
|
// Apply custom function |
546
|
|
|
if ($next_key === '!$') { |
547
|
|
|
if ($is_collection) { |
548
|
|
|
list($data, $path, $functions, $next_key) = self::_arr_get_traverse_apply_custom_function($data, $functions, $path); |
549
|
|
|
} else { |
550
|
|
|
throw new Exception("Invalid path trying to invoke function on non-collection"); |
551
|
|
|
} |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
// Select data either by index or key |
555
|
|
|
if ($plug_index === null) { |
556
|
|
|
$next_node = self::_arr_get_traverse_next_node_key($data, $is_collection, $next_key); |
557
|
|
|
} else { |
558
|
|
|
if ($is_collection) { |
559
|
|
|
$next_node = self::_arr_get_traverse_next_node_index($data, $plug_index); |
560
|
|
|
} else { |
561
|
|
|
throw new Exception("Invalid path trying to plug item but data is not a collection"); |
562
|
|
|
} |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
// If nothing matched break path and return |
566
|
|
|
if (empty($next_node)) { |
567
|
|
|
return null; |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
// If path is at the end return |
571
|
|
|
if (count($path) === 0) { |
572
|
|
|
if (is_array($next_node) && $collapse) { |
573
|
|
|
return array_filter($next_node); |
574
|
|
|
} |
575
|
|
|
return $next_node; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
// If path is not completed |
579
|
|
|
if (is_array($next_node)) { |
580
|
|
|
|
581
|
|
|
// Recurse |
582
|
|
|
$node_is_collection = self::arr_is_collection($next_node); |
583
|
|
|
$node_depth = self::arr_depth($next_node); |
584
|
|
|
|
585
|
|
|
if ($node_is_collection) { |
586
|
|
|
// Collapse collections |
587
|
|
|
if ($collapse // if enabled |
588
|
|
|
&& !is_numeric($path[0]) // if next path segment is not an index |
589
|
|
|
&& $path[0] !== "!$" // if not the result of a custom function |
590
|
|
|
&& $node_depth > 0 // if array of arrays |
591
|
|
|
) { |
592
|
|
|
$next_node = self::arr_collapse($next_node); |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
if (is_numeric($path[0]) && $node_depth < 1) { |
596
|
|
|
$result = self::_arr_get_traverse($next_node, $path, $collapse, $functions); |
597
|
|
|
} else { |
598
|
|
|
// Collect data from sub-tree |
599
|
|
|
$result = []; |
600
|
|
|
foreach ($next_node as $node) { |
601
|
|
|
if ($node === null) { |
602
|
|
|
$result[] = null; |
603
|
|
|
} else { |
604
|
|
|
$partial = self::_arr_get_traverse($node, $path, $collapse, $functions); |
605
|
|
|
if ($collapse) { |
606
|
|
|
$result[] = $partial; |
607
|
|
|
} else { |
608
|
|
|
$result[] = [$partial]; |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
// Since collection functions inject an array segment we must collapse the result |
615
|
|
|
if ($path[0] === "!$") { |
616
|
|
|
$result = self::arr_collapse($result); |
617
|
|
|
} |
618
|
|
|
} else { |
619
|
|
|
$result = self::_arr_get_traverse($next_node, $path, $collapse, $functions); |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
// Collapse result if needed |
623
|
|
|
if (is_array($result)) { |
624
|
|
|
// Collapse collections greater than 1 |
625
|
|
|
if (self::arr_depth($result) > 1) { |
626
|
|
|
$result = self::arr_collapse($result); |
627
|
|
|
} |
628
|
|
|
return array_filter($result); |
629
|
|
|
} |
630
|
|
|
return $result; |
631
|
|
|
} |
632
|
|
|
throw new Exception("Next node in path is not an array"); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/* arr_get: Find next node by index */ |
636
|
|
|
private static function _arr_get_traverse_next_node_index($data, $plug_index) |
637
|
|
|
{ |
638
|
|
|
// Adjust negative index |
639
|
|
|
if ($plug_index < 0) { |
640
|
|
|
$count = count($data); |
641
|
|
|
$plug_index = $count === 1 ? 0 : $count + ($plug_index % $count); |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
// Plug data |
645
|
|
|
if (isset($data[$plug_index])) { |
646
|
|
|
return $data[$plug_index]; |
647
|
|
|
} |
648
|
|
|
return null; |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
/* arr_get: Find next node by key */ |
652
|
|
|
private static function _arr_get_traverse_next_node_key($data, $is_collection, $next_key) |
653
|
|
|
{ |
654
|
|
|
if ($next_key === null) { |
655
|
|
|
return $data; |
656
|
|
|
} |
657
|
|
|
if ($is_collection) { |
658
|
|
|
return array_map(function($item) use ($next_key) { |
659
|
|
|
if ($item !== null && array_key_exists($next_key, $item)) { |
660
|
|
|
return $item[$next_key]; |
661
|
|
|
} |
662
|
|
|
return null; |
663
|
|
|
}, $data); |
664
|
|
|
} else if (is_array($data)) { |
665
|
|
|
if (array_key_exists($next_key, $data)) { |
666
|
|
|
return $data[$next_key]; |
667
|
|
|
} |
668
|
|
|
return null; |
669
|
|
|
} |
670
|
|
|
throw new Exception("Path ...$next_key does not exist"); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
/* arr_get: Invoke custom filter function on path */ |
674
|
|
|
private static function _arr_get_traverse_apply_custom_function($data, $functions, $path) |
675
|
|
|
{ |
676
|
|
|
$function = array_shift($functions); |
677
|
|
|
$data = array_values(array_filter($data, $function, ARRAY_FILTER_USE_BOTH)); |
678
|
|
|
$next_key = array_shift($path); |
679
|
|
|
return [$data, $path, $functions, $next_key]; |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
private static function arr_is_collection($mixed) |
683
|
|
|
{ |
684
|
|
|
return is_array($mixed) && array_values($mixed) === $mixed; |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Return the depth of a collection hiearchy. Zero based. |
689
|
|
|
* |
690
|
|
|
* @param array A collection |
691
|
|
|
* @return int `null` if $array is not a collection. |
692
|
|
|
*/ |
693
|
|
|
private static function arr_depth($array) |
694
|
|
|
{ |
695
|
|
|
if (empty($array) && is_array($array)) return 0; |
696
|
|
|
if (!self::arr_is_collection($array)) return null; |
697
|
|
|
|
698
|
|
|
$depth = 0; |
699
|
|
|
$child = array_shift($array); |
700
|
|
|
while (self::arr_is_collection($child)) { |
701
|
|
|
$depth += 1; |
702
|
|
|
$child = array_shift($child); |
703
|
|
|
} |
704
|
|
|
return $depth; |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
/** |
708
|
|
|
* Partion the input based on the result of the callback function. |
709
|
|
|
* |
710
|
|
|
* @param array $array A collection |
711
|
|
|
* @param \Closeure $callable A callable returning true or false depending on which way to partion the element—left or right. |
712
|
|
|
* @return array An array with two arrays—left and right: [left, right] |
713
|
|
|
*/ |
714
|
|
|
private static function arr_partition($array, Closure $callable) |
715
|
|
|
{ |
716
|
|
|
$left = []; |
717
|
|
|
$right = []; |
718
|
|
|
array_walk($array, function($item, $key) use (&$left, &$right, $callable) { |
719
|
|
|
if ($callable($item, $key)) { |
720
|
|
|
$left[] = $item; |
721
|
|
|
} else { |
722
|
|
|
$right[] = $item; |
723
|
|
|
} |
724
|
|
|
}); |
725
|
|
|
return [$left, $right]; |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
private static function arr_even($array) |
729
|
|
|
{ |
730
|
|
|
return self::arr_partition($array, function($item, $key) { return $key % 2 === 0; })[0]; |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
private static function arr_odd($array) |
734
|
|
|
{ |
735
|
|
|
return self::arr_partition($array, function($item, $key) { return $key % 2 === 1; })[0]; |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/* Synonym of shift */ |
739
|
|
|
private static function arr_head($array) |
740
|
|
|
{ |
741
|
|
|
return self::shift($array); |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
private static function arr_first($array) |
745
|
|
|
{ |
746
|
|
|
if (count($array)) { |
747
|
|
|
return $array[0]; |
748
|
|
|
} |
749
|
|
|
return null; |
750
|
|
|
} |
751
|
|
|
|
752
|
|
|
private static function arr_last($array) |
753
|
|
|
{ |
754
|
|
|
if (count($array)) { |
755
|
|
|
return $array[count($array) - 1]; |
756
|
|
|
} |
757
|
|
|
return null; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
private static function arr_tail($array) |
761
|
|
|
{ |
762
|
|
|
return self::chain($array)->keep()->shift()->toArray(); |
|
|
|
|
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
// _arrgh |
766
|
|
|
static private $arr_functions = [ |
767
|
|
|
"collapse", |
768
|
|
|
"contains", |
769
|
|
|
"except", |
770
|
|
|
"map_assoc", |
771
|
|
|
"only", |
772
|
|
|
"sort_by", |
773
|
|
|
'depth', |
774
|
|
|
'even', |
775
|
|
|
'first', |
776
|
|
|
'get', |
777
|
|
|
'head', |
778
|
|
|
'is_collection', |
779
|
|
|
'last', |
780
|
|
|
'odd', |
781
|
|
|
'partition', |
782
|
|
|
'tail', |
783
|
|
|
]; |
784
|
|
|
|
785
|
|
|
// _call |
786
|
|
|
static private $simple_functions = [ |
787
|
|
|
"array_change_key_case", |
788
|
|
|
"array_chunk", |
789
|
|
|
"array_column", |
790
|
|
|
"array_combine", |
791
|
|
|
"array_count_values", |
792
|
|
|
"array_diff", |
793
|
|
|
"array_diff_assoc", |
794
|
|
|
"array_diff_key", |
795
|
|
|
"array_diff_uassoc", |
796
|
|
|
"array_diff_ukey", |
797
|
|
|
"array_fill", |
798
|
|
|
"array_fill_keys", |
799
|
|
|
"array_filter", |
800
|
|
|
"array_flip", |
801
|
|
|
"array_intersect", |
802
|
|
|
"array_intersect_assoc", |
803
|
|
|
"array_intersect_key", |
804
|
|
|
"array_intersect_uassoc", |
805
|
|
|
"array_intersect_ukey", |
806
|
|
|
"array_keys", |
807
|
|
|
"array_merge", |
808
|
|
|
"array_merge_recursive", |
809
|
|
|
"array_pad", |
810
|
|
|
"array_product", |
811
|
|
|
"array_rand", |
812
|
|
|
"array_reduce", |
813
|
|
|
"array_replace", |
814
|
|
|
"array_replace_recursive", |
815
|
|
|
"array_reverse", |
816
|
|
|
"array_slice", |
817
|
|
|
"array_sum", |
818
|
|
|
"array_udiff", |
819
|
|
|
"array_udiff_assoc", |
820
|
|
|
"array_udiff_uassoc", |
821
|
|
|
"array_uintersect", |
822
|
|
|
"array_uintersect_assoc", |
823
|
|
|
"array_uintersect_uassoc", |
824
|
|
|
"array_unique", |
825
|
|
|
"array_values", |
826
|
|
|
"count", |
827
|
|
|
"max", |
828
|
|
|
"min", |
829
|
|
|
"range", |
830
|
|
|
"sizeof", |
831
|
|
|
]; |
832
|
|
|
|
833
|
|
|
// _copy |
834
|
|
|
static private $mutable_functions = [ |
835
|
|
|
"array_push", |
836
|
|
|
"array_splice", |
837
|
|
|
"array_unshift", |
838
|
|
|
"array_walk", |
839
|
|
|
"array_walk_recursive", |
840
|
|
|
"arsort", |
841
|
|
|
"asort", |
842
|
|
|
"krsort", |
843
|
|
|
"ksort", |
844
|
|
|
"natcasesort", |
845
|
|
|
"natsort", |
846
|
|
|
"rsort", |
847
|
|
|
"shuffle", |
848
|
|
|
"sort", |
849
|
|
|
"uasort", |
850
|
|
|
"uksort", |
851
|
|
|
"usort", |
852
|
|
|
]; |
853
|
|
|
|
854
|
|
|
// _copyMultiple |
855
|
|
|
static private $mutable_functions_multiple = [ |
856
|
|
|
"array_multisort", |
857
|
|
|
]; |
858
|
|
|
|
859
|
|
|
// _copyValue |
860
|
|
|
static private $mutable_value_functions = [ |
861
|
|
|
"array_pop", |
862
|
|
|
"array_shift", |
863
|
|
|
"end", |
864
|
|
|
]; |
865
|
|
|
|
866
|
|
|
// _rotateRight |
867
|
|
|
static private $reverse_functions = [ |
868
|
|
|
"array_map", |
869
|
|
|
]; |
870
|
|
|
|
871
|
|
|
// _swapTwoFirst |
872
|
|
|
static private $swapped_functions = [ |
873
|
|
|
"array_key_exists", |
874
|
|
|
"array_search", |
875
|
|
|
"implode", |
876
|
|
|
"in_array", |
877
|
|
|
"join", |
878
|
|
|
]; |
879
|
|
|
|
880
|
|
|
static private $starters = [ |
881
|
|
|
"array_fill", |
882
|
|
|
"array_fill_keys", |
883
|
|
|
"range", |
884
|
|
|
]; |
885
|
|
|
|
886
|
|
|
static private $terminators = [ |
887
|
|
|
"array_pop", |
888
|
|
|
"array_shift", |
889
|
|
|
"array_sum", |
890
|
|
|
"count", |
891
|
|
|
"first", |
892
|
|
|
"head", |
893
|
|
|
"join", |
894
|
|
|
"last", |
895
|
|
|
"max", |
896
|
|
|
"min", |
897
|
|
|
"sizeof", |
898
|
|
|
]; |
899
|
|
|
|
900
|
|
|
static private $reverse_result_functions = [ |
901
|
|
|
"uasort", |
902
|
|
|
"uksort", |
903
|
|
|
"usort", |
904
|
|
|
"asort", |
905
|
|
|
]; |
906
|
|
|
} |
907
|
|
|
|
908
|
|
|
if (defined("ARRGH")) { |
909
|
|
|
require __DIR__ . '/arrgh_functions.php'; |
910
|
|
|
} |
911
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.