friendship ended with social-app. php is my new best friend
1<?php
2
3/**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7
8declare(strict_types=1);
9
10namespace Nette\Utils;
11
12use JetBrains\PhpStorm\Language;
13use Nette;
14use function array_combine, array_intersect_key, array_is_list, array_key_exists, array_key_first, array_key_last, array_keys, array_reverse, array_search, array_slice, array_walk_recursive, count, func_num_args, in_array, is_array, is_int, is_object, key, preg_split, range;
15use const PHP_VERSION_ID, PREG_GREP_INVERT, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
16
17
18/**
19 * Array tools library.
20 */
21class Arrays
22{
23 use Nette\StaticClass;
24
25 /**
26 * Returns item from array. If it does not exist, it throws an exception, unless a default value is set.
27 * @template T
28 * @param array<T> $array
29 * @param array-key|array-key[] $key
30 * @param ?T $default
31 * @return ?T
32 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
33 */
34 public static function get(array $array, string|int|array $key, mixed $default = null): mixed
35 {
36 foreach (is_array($key) ? $key : [$key] as $k) {
37 if (is_array($array) && array_key_exists($k, $array)) {
38 $array = $array[$k];
39 } else {
40 if (func_num_args() < 3) {
41 throw new Nette\InvalidArgumentException("Missing item '$k'.");
42 }
43
44 return $default;
45 }
46 }
47
48 return $array;
49 }
50
51
52 /**
53 * Returns reference to array item. If the index does not exist, new one is created with value null.
54 * @template T
55 * @param array<T> $array
56 * @param array-key|array-key[] $key
57 * @return ?T
58 * @throws Nette\InvalidArgumentException if traversed item is not an array
59 */
60 public static function &getRef(array &$array, string|int|array $key): mixed
61 {
62 foreach (is_array($key) ? $key : [$key] as $k) {
63 if (is_array($array) || $array === null) {
64 $array = &$array[$k];
65 } else {
66 throw new Nette\InvalidArgumentException('Traversed item is not an array.');
67 }
68 }
69
70 return $array;
71 }
72
73
74 /**
75 * Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as
76 * the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains
77 * the value from the first array in the case of a key collision.
78 * @template T1
79 * @template T2
80 * @param array<T1> $array1
81 * @param array<T2> $array2
82 * @return array<T1|T2>
83 */
84 public static function mergeTree(array $array1, array $array2): array
85 {
86 $res = $array1 + $array2;
87 foreach (array_intersect_key($array1, $array2) as $k => $v) {
88 if (is_array($v) && is_array($array2[$k])) {
89 $res[$k] = self::mergeTree($v, $array2[$k]);
90 }
91 }
92
93 return $res;
94 }
95
96
97 /**
98 * Returns zero-indexed position of given array key. Returns null if key is not found.
99 */
100 public static function getKeyOffset(array $array, string|int $key): ?int
101 {
102 return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true));
103 }
104
105
106 /**
107 * @deprecated use getKeyOffset()
108 */
109 public static function searchKey(array $array, $key): ?int
110 {
111 return self::getKeyOffset($array, $key);
112 }
113
114
115 /**
116 * Tests an array for the presence of value.
117 */
118 public static function contains(array $array, mixed $value): bool
119 {
120 return in_array($value, $array, true);
121 }
122
123
124 /**
125 * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
126 * @template K of int|string
127 * @template V
128 * @param array<K, V> $array
129 * @param ?callable(V, K, array<K, V>): bool $predicate
130 * @return ?V
131 */
132 public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
133 {
134 $key = self::firstKey($array, $predicate);
135 return $key === null
136 ? ($else ? $else() : null)
137 : $array[$key];
138 }
139
140
141 /**
142 * Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
143 * @template K of int|string
144 * @template V
145 * @param array<K, V> $array
146 * @param ?callable(V, K, array<K, V>): bool $predicate
147 * @return ?V
148 */
149 public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
150 {
151 $key = self::lastKey($array, $predicate);
152 return $key === null
153 ? ($else ? $else() : null)
154 : $array[$key];
155 }
156
157
158 /**
159 * Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
160 * @template K of int|string
161 * @template V
162 * @param array<K, V> $array
163 * @param ?callable(V, K, array<K, V>): bool $predicate
164 * @return ?K
165 */
166 public static function firstKey(array $array, ?callable $predicate = null): int|string|null
167 {
168 if (!$predicate) {
169 return array_key_first($array);
170 }
171 foreach ($array as $k => $v) {
172 if ($predicate($v, $k, $array)) {
173 return $k;
174 }
175 }
176 return null;
177 }
178
179
180 /**
181 * Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
182 * @template K of int|string
183 * @template V
184 * @param array<K, V> $array
185 * @param ?callable(V, K, array<K, V>): bool $predicate
186 * @return ?K
187 */
188 public static function lastKey(array $array, ?callable $predicate = null): int|string|null
189 {
190 return $predicate
191 ? self::firstKey(array_reverse($array, preserve_keys: true), $predicate)
192 : array_key_last($array);
193 }
194
195
196 /**
197 * Inserts the contents of the $inserted array into the $array immediately after the $key.
198 * If $key is null (or does not exist), it is inserted at the beginning.
199 */
200 public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
201 {
202 $offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
203 $array = array_slice($array, 0, $offset, preserve_keys: true)
204 + $inserted
205 + array_slice($array, $offset, count($array), preserve_keys: true);
206 }
207
208
209 /**
210 * Inserts the contents of the $inserted array into the $array before the $key.
211 * If $key is null (or does not exist), it is inserted at the end.
212 */
213 public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
214 {
215 if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
216 $offset = count($array) - 1;
217 }
218
219 $array = array_slice($array, 0, $offset + 1, preserve_keys: true)
220 + $inserted
221 + array_slice($array, $offset + 1, count($array), preserve_keys: true);
222 }
223
224
225 /**
226 * Renames key in array.
227 */
228 public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
229 {
230 $offset = self::getKeyOffset($array, $oldKey);
231 if ($offset === null) {
232 return false;
233 }
234
235 $val = &$array[$oldKey];
236 $keys = array_keys($array);
237 $keys[$offset] = $newKey;
238 $array = array_combine($keys, $array);
239 $array[$newKey] = &$val;
240 return true;
241 }
242
243
244 /**
245 * Returns only those array items, which matches a regular expression $pattern.
246 * @param string[] $array
247 * @return string[]
248 */
249 public static function grep(
250 array $array,
251 #[Language('RegExp')]
252 string $pattern,
253 bool|int $invert = false,
254 ): array
255 {
256 $flags = $invert ? PREG_GREP_INVERT : 0;
257 return Strings::pcre('preg_grep', [$pattern, $array, $flags]);
258 }
259
260
261 /**
262 * Transforms multidimensional array to flat array.
263 */
264 public static function flatten(array $array, bool $preserveKeys = false): array
265 {
266 $res = [];
267 $cb = $preserveKeys
268 ? function ($v, $k) use (&$res): void { $res[$k] = $v; }
269 : function ($v) use (&$res): void { $res[] = $v; };
270 array_walk_recursive($array, $cb);
271 return $res;
272 }
273
274
275 /**
276 * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
277 * @return ($value is list ? true : false)
278 */
279 public static function isList(mixed $value): bool
280 {
281 return is_array($value) && (
282 PHP_VERSION_ID < 80100
283 ? !$value || array_keys($value) === range(0, count($value) - 1)
284 : array_is_list($value)
285 );
286 }
287
288
289 /**
290 * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
291 * @param string|string[] $path
292 */
293 public static function associate(array $array, $path): array|\stdClass
294 {
295 $parts = is_array($path)
296 ? $path
297 : preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
298
299 if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
300 throw new Nette\InvalidArgumentException("Invalid path '$path'.");
301 }
302
303 $res = $parts[0] === '->' ? new \stdClass : [];
304
305 foreach ($array as $rowOrig) {
306 $row = (array) $rowOrig;
307 $x = &$res;
308
309 for ($i = 0; $i < count($parts); $i++) {
310 $part = $parts[$i];
311 if ($part === '[]') {
312 $x = &$x[];
313
314 } elseif ($part === '=') {
315 if (isset($parts[++$i])) {
316 $x = $row[$parts[$i]];
317 $row = null;
318 }
319 } elseif ($part === '->') {
320 if (isset($parts[++$i])) {
321 if ($x === null) {
322 $x = new \stdClass;
323 }
324
325 $x = &$x->{$row[$parts[$i]]};
326 } else {
327 $row = is_object($rowOrig) ? $rowOrig : (object) $row;
328 }
329 } elseif ($part !== '|') {
330 $x = &$x[(string) $row[$part]];
331 }
332 }
333
334 if ($x === null) {
335 $x = $row;
336 }
337 }
338
339 return $res;
340 }
341
342
343 /**
344 * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
345 */
346 public static function normalize(array $array, mixed $filling = null): array
347 {
348 $res = [];
349 foreach ($array as $k => $v) {
350 $res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v;
351 }
352
353 return $res;
354 }
355
356
357 /**
358 * Returns and removes the value of an item from an array. If it does not exist, it throws an exception,
359 * or returns $default, if provided.
360 * @template T
361 * @param array<T> $array
362 * @param ?T $default
363 * @return ?T
364 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
365 */
366 public static function pick(array &$array, string|int $key, mixed $default = null): mixed
367 {
368 if (array_key_exists($key, $array)) {
369 $value = $array[$key];
370 unset($array[$key]);
371 return $value;
372
373 } elseif (func_num_args() < 3) {
374 throw new Nette\InvalidArgumentException("Missing item '$key'.");
375
376 } else {
377 return $default;
378 }
379 }
380
381
382 /**
383 * Tests whether at least one element in the array passes the test implemented by the provided function.
384 * @template K of int|string
385 * @template V
386 * @param array<K, V> $array
387 * @param callable(V, K, array<K, V>): bool $predicate
388 */
389 public static function some(iterable $array, callable $predicate): bool
390 {
391 foreach ($array as $k => $v) {
392 if ($predicate($v, $k, $array)) {
393 return true;
394 }
395 }
396
397 return false;
398 }
399
400
401 /**
402 * Tests whether all elements in the array pass the test implemented by the provided function.
403 * @template K of int|string
404 * @template V
405 * @param array<K, V> $array
406 * @param callable(V, K, array<K, V>): bool $predicate
407 */
408 public static function every(iterable $array, callable $predicate): bool
409 {
410 foreach ($array as $k => $v) {
411 if (!$predicate($v, $k, $array)) {
412 return false;
413 }
414 }
415
416 return true;
417 }
418
419
420 /**
421 * Returns a new array containing all key-value pairs matching the given $predicate.
422 * @template K of int|string
423 * @template V
424 * @param array<K, V> $array
425 * @param callable(V, K, array<K, V>): bool $predicate
426 * @return array<K, V>
427 */
428 public static function filter(array $array, callable $predicate): array
429 {
430 $res = [];
431 foreach ($array as $k => $v) {
432 if ($predicate($v, $k, $array)) {
433 $res[$k] = $v;
434 }
435 }
436 return $res;
437 }
438
439
440 /**
441 * Returns an array containing the original keys and results of applying the given transform function to each element.
442 * @template K of int|string
443 * @template V
444 * @template R
445 * @param array<K, V> $array
446 * @param callable(V, K, array<K, V>): R $transformer
447 * @return array<K, R>
448 */
449 public static function map(iterable $array, callable $transformer): array
450 {
451 $res = [];
452 foreach ($array as $k => $v) {
453 $res[$k] = $transformer($v, $k, $array);
454 }
455
456 return $res;
457 }
458
459
460 /**
461 * Returns an array containing new keys and values generated by applying the given transform function to each element.
462 * If the function returns null, the element is skipped.
463 * @template K of int|string
464 * @template V
465 * @template ResK of int|string
466 * @template ResV
467 * @param array<K, V> $array
468 * @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer
469 * @return array<ResK, ResV>
470 */
471 public static function mapWithKeys(array $array, callable $transformer): array
472 {
473 $res = [];
474 foreach ($array as $k => $v) {
475 $pair = $transformer($v, $k, $array);
476 if ($pair) {
477 $res[$pair[0]] = $pair[1];
478 }
479 }
480
481 return $res;
482 }
483
484
485 /**
486 * Invokes all callbacks and returns array of results.
487 * @param callable[] $callbacks
488 */
489 public static function invoke(iterable $callbacks, ...$args): array
490 {
491 $res = [];
492 foreach ($callbacks as $k => $cb) {
493 $res[$k] = $cb(...$args);
494 }
495
496 return $res;
497 }
498
499
500 /**
501 * Invokes method on every object in an array and returns array of results.
502 * @param object[] $objects
503 */
504 public static function invokeMethod(iterable $objects, string $method, ...$args): array
505 {
506 $res = [];
507 foreach ($objects as $k => $obj) {
508 $res[$k] = $obj->$method(...$args);
509 }
510
511 return $res;
512 }
513
514
515 /**
516 * Copies the elements of the $array array to the $object object and then returns it.
517 * @template T of object
518 * @param T $object
519 * @return T
520 */
521 public static function toObject(iterable $array, object $object): object
522 {
523 foreach ($array as $k => $v) {
524 $object->$k = $v;
525 }
526
527 return $object;
528 }
529
530
531 /**
532 * Converts value to array key.
533 */
534 public static function toKey(mixed $value): int|string
535 {
536 return key([$value => null]);
537 }
538
539
540 /**
541 * Returns copy of the $array where every item is converted to string
542 * and prefixed by $prefix and suffixed by $suffix.
543 * @param string[] $array
544 * @return string[]
545 */
546 public static function wrap(array $array, string $prefix = '', string $suffix = ''): array
547 {
548 $res = [];
549 foreach ($array as $k => $v) {
550 $res[$k] = $prefix . $v . $suffix;
551 }
552
553 return $res;
554 }
555}