1: <?php declare(strict_types=1);
2:
3: namespace Salient\Utility;
4:
5: use Salient\Contract\Core\Jsonable;
6: use ArrayAccess;
7: use OutOfRangeException;
8: use Stringable;
9:
10: /**
11: * Work with arrays and iterables
12: *
13: * @api
14: */
15: final class Arr extends AbstractUtility
16: {
17: public const SORT_REGULAR = \SORT_REGULAR;
18: public const SORT_NUMERIC = \SORT_NUMERIC;
19: public const SORT_STRING = \SORT_STRING;
20: public const SORT_LOCALE_STRING = \SORT_LOCALE_STRING;
21: public const SORT_NATURAL = \SORT_NATURAL;
22: public const SORT_FLAG_CASE = \SORT_FLAG_CASE;
23:
24: /**
25: * Get values from a list of arrays using dot notation
26: *
27: * @param iterable<mixed[]> $array
28: * @return ($keyKey is null ? list<mixed> : mixed[])
29: */
30: public static function pluck(iterable $array, string $valueKey, ?string $keyKey = null): array
31: {
32: foreach ($array as $item) {
33: $value = self::get($item, $valueKey);
34: if ($keyKey === null) {
35: $plucked[] = $value;
36: continue;
37: }
38: $key = self::get($item, $keyKey);
39: $plucked[$key] = $value;
40: }
41: return $plucked ?? [];
42: }
43:
44: /**
45: * Get a value from an array using dot notation
46: *
47: * @param mixed[] $array
48: * @param mixed $default
49: * @return mixed
50: * @throws OutOfRangeException if `$key` is not found in `$array` and no
51: * `$default` is given.
52: */
53: public static function get(array $array, string $key, $default = null)
54: {
55: foreach (explode('.', $key) as $part) {
56: if (is_array($array) && array_key_exists($part, $array)) {
57: $array = $array[$part];
58: continue;
59: }
60: if (func_num_args() < 3) {
61: throw new OutOfRangeException(sprintf(
62: 'Array key not found: %s',
63: $key,
64: ));
65: }
66: return $default;
67: }
68: return $array;
69: }
70:
71: /**
72: * Check if a value exists in an array using dot notation
73: *
74: * @param mixed[] $array
75: */
76: public static function has(array $array, string $key): bool
77: {
78: foreach (explode('.', $key) as $part) {
79: if (!is_array($array) || !array_key_exists($part, $array)) {
80: return false;
81: }
82: $array = $array[$part];
83: }
84: return true;
85: }
86:
87: /**
88: * Get the first value in an array
89: *
90: * @template TValue
91: *
92: * @param TValue[] $array
93: * @return ($array is non-empty-array ? TValue : null)
94: */
95: public static function first(array $array)
96: {
97: if ($array === []) {
98: return null;
99: }
100: return reset($array);
101: }
102:
103: /**
104: * Get the last value in an array
105: *
106: * @template TValue
107: *
108: * @param TValue[] $array
109: * @return ($array is non-empty-array ? TValue : null)
110: */
111: public static function last(array $array)
112: {
113: if ($array === []) {
114: return null;
115: }
116: return end($array);
117: }
118:
119: /**
120: * Get the key of a value in an array
121: *
122: * @template TKey of array-key
123: * @template TValue
124: *
125: * @param array<TKey,TValue> $array
126: * @param TValue $value
127: * @return TKey
128: * @throws OutOfRangeException if `$value` is not found in `$array`.
129: */
130: public static function keyOf(array $array, $value)
131: {
132: $key = array_search($value, $array, true);
133: if ($key === false) {
134: throw new OutOfRangeException('Value not found in array');
135: }
136: /** @var TKey */
137: return $key;
138: }
139:
140: /**
141: * Shift an element off the beginning of an array
142: *
143: * @template TKey of array-key
144: * @template TValue
145: *
146: * @param array<TKey,TValue> $array
147: * @param TValue|null $shifted
148: * @param-out ($array is non-empty-array ? TValue : null) $shifted
149: * @return array<TKey,TValue>
150: */
151: public static function shift(array $array, &$shifted = null): array
152: {
153: $shifted = array_shift($array);
154: return $array;
155: }
156:
157: /**
158: * Add elements to the beginning of an array
159: *
160: * @template TKey of array-key
161: * @template TValue
162: *
163: * @param array<TKey,TValue> $array
164: * @param TValue ...$values
165: * @return array<TKey|int,TValue>
166: */
167: public static function unshift(array $array, ...$values): array
168: {
169: array_unshift($array, ...$values);
170: return $array;
171: }
172:
173: /**
174: * Pop a value off the end of an array
175: *
176: * @template TKey of array-key
177: * @template TValue
178: *
179: * @param array<TKey,TValue> $array
180: * @param TValue|null $popped
181: * @param-out ($array is non-empty-array ? TValue : null) $popped
182: * @return array<TKey,TValue>
183: */
184: public static function pop(array $array, &$popped = null): array
185: {
186: $popped = array_pop($array);
187: return $array;
188: }
189:
190: /**
191: * Push values onto the end of an array
192: *
193: * @template TKey of array-key
194: * @template TValue
195: *
196: * @param array<TKey,TValue> $array
197: * @param TValue ...$values
198: * @return array<TKey|int,TValue>
199: */
200: public static function push(array $array, ...$values): array
201: {
202: array_push($array, ...$values);
203: return $array;
204: }
205:
206: /**
207: * Push values onto the end of an array after excluding any that are already
208: * present
209: *
210: * @template TKey of array-key
211: * @template TValue
212: *
213: * @param array<TKey,TValue> $array
214: * @param TValue ...$values
215: * @return array<TKey|int,TValue>
216: */
217: public static function extend(array $array, ...$values): array
218: {
219: return array_merge($array, array_diff($values, $array));
220: }
221:
222: /**
223: * Assign a value to an element of an array
224: *
225: * @template TKey of array-key
226: * @template TValue
227: *
228: * @param array<TKey,TValue> $array
229: * @param TKey $key
230: * @param TValue $value
231: * @return array<TKey,TValue>
232: */
233: public static function set(array $array, $key, $value): array
234: {
235: $array[$key] = $value;
236: return $array;
237: }
238:
239: /**
240: * Assign a value to an element of an array if it isn't already set
241: *
242: * @template TKey of array-key
243: * @template TValue
244: *
245: * @param array<TKey,TValue> $array
246: * @param TKey $key
247: * @param TValue $value
248: * @return array<TKey,TValue>
249: */
250: public static function setIf(array $array, $key, $value): array
251: {
252: if (!array_key_exists($key, $array)) {
253: $array[$key] = $value;
254: }
255: return $array;
256: }
257:
258: /**
259: * Remove a key from an array
260: *
261: * @template TKey of array-key
262: * @template TValue
263: *
264: * @param array<TKey,TValue> $array
265: * @param TKey $key
266: * @return array<TKey,TValue>
267: */
268: public static function unset(array $array, $key): array
269: {
270: unset($array[$key]);
271: return $array;
272: }
273:
274: /**
275: * Sort an array by value
276: *
277: * @template TKey of array-key
278: * @template TValue
279: *
280: * @param array<TKey,TValue> $array
281: * @param (callable(TValue, TValue): int)|int-mask-of<Arr::SORT_*> $callbackOrFlags
282: * @return ($preserveKeys is true ? array<TKey,TValue> : list<TValue>)
283: */
284: public static function sort(
285: array $array,
286: bool $preserveKeys = false,
287: $callbackOrFlags = \SORT_REGULAR
288: ): array {
289: if (is_callable($callbackOrFlags)) {
290: if ($preserveKeys) {
291: uasort($array, $callbackOrFlags);
292: return $array;
293: }
294: usort($array, $callbackOrFlags);
295: return $array;
296: }
297: if ($preserveKeys) {
298: asort($array, $callbackOrFlags);
299: return $array;
300: }
301: sort($array, $callbackOrFlags);
302: return $array;
303: }
304:
305: /**
306: * Sort an array by value in descending order
307: *
308: * @template TKey of array-key
309: * @template TValue
310: *
311: * @param array<TKey,TValue> $array
312: * @param int-mask-of<Arr::SORT_*> $flags
313: * @return ($preserveKeys is true ? array<TKey,TValue> : list<TValue>)
314: */
315: public static function sortDesc(
316: array $array,
317: bool $preserveKeys = false,
318: int $flags = \SORT_REGULAR
319: ): array {
320: if ($preserveKeys) {
321: arsort($array, $flags);
322: return $array;
323: }
324: rsort($array, $flags);
325: return $array;
326: }
327:
328: /**
329: * Sort an array by key
330: *
331: * @template TKey of array-key
332: * @template TValue
333: *
334: * @param array<TKey,TValue> $array
335: * @param (callable(TKey, TKey): int)|int-mask-of<Arr::SORT_*> $callbackOrFlags
336: * @return array<TKey,TValue>
337: */
338: public static function sortByKey(
339: array $array,
340: $callbackOrFlags = \SORT_REGULAR
341: ): array {
342: if (is_callable($callbackOrFlags)) {
343: uksort($array, $callbackOrFlags);
344: return $array;
345: }
346: ksort($array, $callbackOrFlags);
347: return $array;
348: }
349:
350: /**
351: * Sort an array by key in descending order
352: *
353: * @template TKey of array-key
354: * @template TValue
355: *
356: * @param array<TKey,TValue> $array
357: * @param int-mask-of<Arr::SORT_*> $flags
358: * @return array<TKey,TValue>
359: */
360: public static function sortByKeyDesc(
361: array $array,
362: int $flags = \SORT_REGULAR
363: ): array {
364: krsort($array, $flags);
365: return $array;
366: }
367:
368: /**
369: * Remove duplicate values from an array
370: *
371: * @template TKey
372: * @template TValue
373: *
374: * @param iterable<TKey,TValue> $array
375: * @return ($preserveKeys is true ? (TKey is array-key ? array<TKey,TValue> : array<array-key,TValue>) : list<TValue>)
376: */
377: public static function unique(
378: iterable $array,
379: bool $preserveKeys = false
380: ): array {
381: $unique = [];
382: foreach ($array as $key => $value) {
383: if (in_array($value, $unique, true)) {
384: continue;
385: }
386: if ($preserveKeys) {
387: $unique[self::getKey($key, $i)] = $value;
388: } else {
389: $unique[] = $value;
390: }
391: }
392: return $unique;
393: }
394:
395: /**
396: * Check if a value is an array with integer keys numbered consecutively
397: * from 0
398: *
399: * @param mixed $value
400: * @phpstan-assert-if-true list<mixed> $value
401: */
402: public static function isList($value, bool $orEmpty = false): bool
403: {
404: if (!is_array($value)) {
405: return false;
406: }
407: if (!$value) {
408: return $orEmpty;
409: }
410: $i = 0;
411: foreach ($value as $key => $value) {
412: if ($i++ !== $key) {
413: return false;
414: }
415: }
416: return true;
417: }
418:
419: /**
420: * Check if a value is an array with integer keys
421: *
422: * @param mixed $value
423: * @phpstan-assert-if-true array<int,mixed> $value
424: */
425: public static function isIndexed($value, bool $orEmpty = false): bool
426: {
427: if (!is_array($value)) {
428: return false;
429: }
430: if (!$value) {
431: return $orEmpty;
432: }
433: foreach ($value as $key => $value) {
434: if (!is_int($key)) {
435: return false;
436: }
437: }
438: return true;
439: }
440:
441: /**
442: * Check if a value is an array of integers or an array of strings
443: *
444: * @param mixed $value
445: * @phpstan-assert-if-true int[]|string[] $value
446: */
447: public static function ofArrayKey($value, bool $orEmpty = false): bool
448: {
449: return self::ofInt($value, $orEmpty) || self::ofString($value);
450: }
451:
452: /**
453: * Check if a value is an array of integers
454: *
455: * @param mixed $value
456: * @phpstan-assert-if-true int[] $value
457: */
458: public static function ofInt($value, bool $orEmpty = false): bool
459: {
460: return self::doIsArrayOf('is_int', $value, $orEmpty);
461: }
462:
463: /**
464: * Check if a value is an array of strings
465: *
466: * @param mixed $value
467: * @phpstan-assert-if-true string[] $value
468: */
469: public static function ofString($value, bool $orEmpty = false): bool
470: {
471: return self::doIsArrayOf('is_string', $value, $orEmpty);
472: }
473:
474: /**
475: * Check if a value is an array of instances of a given class
476: *
477: * @template T
478: *
479: * @param mixed $value
480: * @param class-string<T> $class
481: * @phpstan-assert-if-true T[] $value
482: */
483: public static function of($value, string $class, bool $orEmpty = false): bool
484: {
485: return self::doIsArrayOf('is_a', $value, $orEmpty, $class);
486: }
487:
488: /**
489: * @param string&callable $func
490: * @param mixed $value
491: * @param mixed ...$args
492: */
493: private static function doIsArrayOf(string $func, $value, bool $orEmpty, ...$args): bool
494: {
495: if (!is_array($value)) {
496: return false;
497: }
498: if (!$value) {
499: return $orEmpty;
500: }
501: foreach ($value as $item) {
502: if (!$func($item, ...$args)) {
503: return false;
504: }
505: }
506: return true;
507: }
508:
509: /**
510: * Check if arrays have the same values after sorting for comparison
511: *
512: * @param mixed[] $array1
513: * @param mixed[] $array2
514: * @param mixed[] ...$arrays
515: */
516: public static function sameValues(array $array1, array $array2, array ...$arrays): bool
517: {
518: $last = null;
519: foreach ([$array1, $array2, ...$arrays] as $array) {
520: usort($array, fn($a, $b) => gettype($a) <=> gettype($b) ?: $a <=> $b);
521: if ($last !== null && $last !== $array) {
522: return false;
523: }
524: $last = $array;
525: }
526: return true;
527: }
528:
529: /**
530: * Check if arrays are the same after sorting by key
531: *
532: * @param mixed[] $array1
533: * @param mixed[] $array2
534: * @param mixed[] ...$arrays
535: */
536: public static function same(array $array1, array $array2, array ...$arrays): bool
537: {
538: $last = null;
539: foreach ([$array1, $array2, ...$arrays] as $array) {
540: ksort($array, \SORT_STRING);
541: if ($last !== null && $last !== $array) {
542: return false;
543: }
544: $last = $array;
545: }
546: return true;
547: }
548:
549: /**
550: * Remove null values from an array
551: *
552: * @template TKey
553: * @template TValue
554: *
555: * @param iterable<TKey,TValue|null> $array
556: * @return (TKey is array-key ? array<TKey,TValue> : array<array-key,TValue>)
557: */
558: public static function whereNotNull(iterable $array): array
559: {
560: foreach ($array as $key => $value) {
561: if ($value === null) {
562: continue;
563: }
564: $filtered[self::getKey($key, $i)] = $value;
565: }
566: return $filtered ?? [];
567: }
568:
569: /**
570: * Remove empty strings from an array of strings and Stringables
571: *
572: * @template TKey
573: * @template TValue of int|float|string|bool|Stringable
574: *
575: * @param iterable<TKey,TValue|null> $array
576: * @return (TKey is array-key ? array<TKey,TValue> : array<array-key,TValue>)
577: */
578: public static function whereNotEmpty(iterable $array): array
579: {
580: foreach ($array as $key => $value) {
581: if ($value === null || (string) $value === '') {
582: continue;
583: }
584: $filtered[self::getKey($key, $i)] = $value;
585: }
586: return $filtered ?? [];
587: }
588:
589: /**
590: * Implode values that remain in an array of strings and Stringables after
591: * trimming characters from each value and removing empty strings
592: *
593: * @param iterable<int|float|string|bool|Stringable|null> $array
594: * @param string|null $characters Characters to trim, `null` (the default)
595: * to trim whitespace, or an empty string to trim nothing.
596: */
597: public static function implode(
598: string $separator,
599: iterable $array,
600: ?string $characters = null
601: ): string {
602: foreach ($array as $value) {
603: $value = (string) $value;
604: if ($characters !== '') {
605: $value = $characters === null
606: ? trim($value)
607: : trim($value, $characters);
608: }
609: if ($value === '') {
610: continue;
611: }
612: $filtered[] = $value;
613: }
614: return implode($separator, $filtered ?? []);
615: }
616:
617: /**
618: * Trim characters from each value in an array of strings and Stringables
619: * before removing or optionally replacing empty strings
620: *
621: * @template TKey
622: *
623: * @param iterable<TKey,int|float|string|bool|Stringable|null> $array
624: * @param string|null $characters Characters to trim, `null` (the default)
625: * to trim whitespace, or an empty string to trim nothing.
626: * @return ($removeEmpty is false ? ($nullEmpty is true ? (TKey is array-key ? array<TKey,string|null> : array<array-key,string|null>) : (TKey is array-key ? array<TKey,string> : array<array-key,string>)) : list<string>)
627: */
628: public static function trim(
629: iterable $array,
630: ?string $characters = null,
631: bool $removeEmpty = true,
632: bool $nullEmpty = false
633: ): array {
634: foreach ($array as $key => $value) {
635: $value = (string) $value;
636: if ($characters !== '') {
637: $value = $characters === null
638: ? trim($value)
639: : trim($value, $characters);
640: }
641: if ($removeEmpty) {
642: if ($value !== '') {
643: $trimmed[] = $value;
644: }
645: continue;
646: }
647: $trimmed[self::getKey($key, $i)] = $nullEmpty && $value === ''
648: ? null
649: : $value;
650: }
651: return $trimmed ?? [];
652: }
653:
654: /**
655: * Make an array of strings and Stringables lowercase
656: *
657: * @template TKey
658: * @template TValue of int|float|string|bool|Stringable|null
659: *
660: * @param iterable<TKey,TValue> $array
661: * @return (TKey is array-key ? array<TKey,string> : array<array-key,string>)
662: */
663: public static function lower(iterable $array): array
664: {
665: foreach ($array as $key => $value) {
666: $lower[self::getKey($key, $i)] = Str::lower((string) $value);
667: }
668: return $lower ?? [];
669: }
670:
671: /**
672: * Make an array of strings and Stringables uppercase
673: *
674: * @template TKey
675: * @template TValue of int|float|string|bool|Stringable|null
676: *
677: * @param iterable<TKey,TValue> $array
678: * @return (TKey is array-key ? array<TKey,string> : array<array-key,string>)
679: */
680: public static function upper(iterable $array): array
681: {
682: foreach ($array as $key => $value) {
683: $upper[self::getKey($key, $i)] = Str::upper((string) $value);
684: }
685: return $upper ?? [];
686: }
687:
688: /**
689: * Make an array of strings and Stringables snake_case
690: *
691: * @template TKey
692: * @template TValue of int|float|string|bool|Stringable|null
693: *
694: * @param iterable<TKey,TValue> $array
695: * @return (TKey is array-key ? array<TKey,string> : array<array-key,string>)
696: */
697: public static function snakeCase(iterable $array): array
698: {
699: foreach ($array as $key => $value) {
700: $snakeCase[self::getKey($key, $i)] = Str::snake((string) $value);
701: }
702: return $snakeCase ?? [];
703: }
704:
705: /**
706: * Cast non-scalar values in an array to strings
707: *
708: * Objects that implement {@see Stringable} are cast to a string. `null`
709: * values are replaced with `$null`. Other non-scalar values are
710: * JSON-encoded.
711: *
712: * @template TKey
713: * @template TValue of int|float|string|bool
714: * @template TNull of int|float|string|bool|null
715: *
716: * @param iterable<TKey,TValue|mixed[]|object|null> $array
717: * @param TNull $null
718: * @return (TKey is array-key ? array<TKey,TValue|TNull|string> : array<array-key,TValue|TNull|string>)
719: */
720: public static function toScalars(iterable $array, $null = null): array
721: {
722: foreach ($array as $key => $value) {
723: if ($value === null) {
724: $value = $null;
725: } elseif (!is_scalar($value)) {
726: if (Test::isStringable($value)) {
727: $value = (string) $value;
728: } elseif ($value instanceof Jsonable) {
729: $value = $value->toJson(Json::ENCODE_FLAGS);
730: } else {
731: $value = Json::stringify($value);
732: }
733: }
734: $scalars[self::getKey($key, $i)] = $value;
735: }
736: return $scalars ?? [];
737: }
738:
739: /**
740: * Cast values in an array to strings
741: *
742: * Scalar values and objects that implement {@see Stringable} are cast to a
743: * string. `null` values are replaced with `$null`. Other non-scalar values
744: * are JSON-encoded.
745: *
746: * @template TKey
747: * @template TNull of string|null
748: *
749: * @param iterable<TKey,mixed[]|object|int|float|string|bool|null> $array
750: * @param TNull $null
751: * @return (TKey is array-key ? array<TKey,TNull|string> : array<array-key,TNull|string>)
752: */
753: public static function toStrings(iterable $array, ?string $null = null): array
754: {
755: foreach ($array as $key => $value) {
756: if ($value === null) {
757: $value = $null;
758: } elseif (is_scalar($value) || Test::isStringable($value)) {
759: $value = (string) $value;
760: } elseif ($value instanceof Jsonable) {
761: $value = $value->toJson(Json::ENCODE_FLAGS);
762: } else {
763: $value = Json::stringify($value);
764: }
765: $strings[self::getKey($key, $i)] = $value;
766: }
767: return $strings ?? [];
768: }
769:
770: /**
771: * Get the offset (0-based) of a key in an array
772: *
773: * @template TKey of array-key
774: *
775: * @param array<TKey,mixed> $array
776: * @param TKey $key
777: * @throws OutOfRangeException if `$key` is not found in `$array`.
778: */
779: public static function offsetOfKey(array $array, $key): int
780: {
781: $offset = array_flip(array_keys($array))[$key] ?? null;
782: if ($offset === null) {
783: throw new OutOfRangeException(sprintf('Array key not found: %s', $key));
784: }
785: return $offset;
786: }
787:
788: /**
789: * Rename an array key without changing its offset
790: *
791: * @template TKey of array-key
792: * @template TValue
793: *
794: * @param array<TKey,TValue> $array
795: * @param TKey $key
796: * @param TKey $newKey
797: * @return array<TKey,TValue>
798: */
799: public static function rename(array $array, $key, $newKey): array
800: {
801: if (!array_key_exists($key, $array)) {
802: throw new OutOfRangeException(sprintf('Array key not found: %s', $key));
803: }
804: if ($key === $newKey) {
805: return $array;
806: }
807: return self::spliceByKey($array, $key, 1, [$newKey => $array[$key]]);
808: }
809:
810: /**
811: * Remove and/or replace part of an array by offset (0-based)
812: *
813: * @template TKey of array-key
814: * @template TValue
815: *
816: * @param array<TKey,TValue> $array
817: * @param TValue[]|TValue $replacement
818: * @param array<TKey,TValue>|null $replaced
819: * @param-out array<TKey,TValue> $replaced
820: * @return array<TKey|int,TValue>
821: */
822: public static function splice(
823: array $array,
824: int $offset,
825: ?int $length = null,
826: $replacement = [],
827: ?array &$replaced = null
828: ): array {
829: // $length can't be null in PHP 7.4
830: if ($length === null) {
831: $length = count($array);
832: }
833: $replaced = array_splice($array, $offset, $length, $replacement);
834: return $array;
835: }
836:
837: /**
838: * Remove and/or replace part of an array by key
839: *
840: * @template TKey of array-key
841: * @template TValue
842: *
843: * @param array<TKey,TValue> $array
844: * @param TKey $key
845: * @param array<TKey,TValue> $replacement
846: * @param array<TKey,TValue>|null $replaced
847: * @param-out array<TKey,TValue> $replaced
848: * @return array<TKey,TValue>
849: */
850: public static function spliceByKey(
851: array $array,
852: $key,
853: ?int $length = null,
854: array $replacement = [],
855: ?array &$replaced = null
856: ): array {
857: $keys = array_keys($array);
858: $offset = array_flip($keys)[$key] ?? null;
859: if ($offset === null) {
860: throw new OutOfRangeException(sprintf('Array key not found: %s', $key));
861: }
862: // $length can't be null in PHP 7.4
863: if ($length === null) {
864: $length = count($array);
865: }
866: $replaced = array_combine(
867: array_splice($keys, $offset, $length, array_keys($replacement)),
868: array_splice($array, $offset, $length, $replacement),
869: );
870: return array_combine($keys, $array);
871: }
872:
873: /**
874: * Get an array that maps the values in an array to a given value
875: *
876: * @template TKey of array-key
877: * @template TValue
878: *
879: * @param iterable<TKey> $array
880: * @param TValue $value
881: * @return ($value is true ? array<TKey,true> : array<TKey,TValue>)
882: */
883: public static function toIndex(iterable $array, $value = true): array
884: {
885: if (is_array($array)) {
886: return array_fill_keys($array, $value);
887: }
888: foreach ($array as $key) {
889: $index[$key] = $value;
890: }
891: return $index ?? [];
892: }
893:
894: /**
895: * Index an array by an identifier unique to each value
896: *
897: * @template TValue of ArrayAccess|mixed[]|object
898: *
899: * @param iterable<TValue> $array
900: * @param array-key $key
901: * @return TValue[]
902: */
903: public static function toMap(iterable $array, $key): array
904: {
905: foreach ($array as $item) {
906: $map[
907: is_array($item) || $item instanceof ArrayAccess
908: ? $item[$key]
909: : $item->$key
910: ] = $item;
911: }
912: return $map ?? [];
913: }
914:
915: /**
916: * Apply a callback to a value for each of the elements of an array
917: *
918: * The return value of each call is passed to the next or returned to the
919: * caller.
920: *
921: * Similar to {@see array_reduce()}.
922: *
923: * @template TKey
924: * @template TValue
925: * @template T
926: *
927: * @param iterable<TKey,TValue> $array
928: * @param callable(T, TValue, TKey): T $callback
929: * @param T $value
930: * @return T
931: */
932: public static function with(iterable $array, callable $callback, $value)
933: {
934: foreach ($array as $arrKey => $arrValue) {
935: $value = $callback($value, $arrValue, $arrKey);
936: }
937: return $value;
938: }
939:
940: /**
941: * Flatten a multi-dimensional array
942: *
943: * @param iterable<mixed> $array
944: * @param int $limit The maximum number of dimensions to flatten, or `-1`
945: * for no limit.
946: * @return mixed[]
947: */
948: public static function flatten(iterable $array, int $limit = -1): array
949: {
950: do {
951: $flattened = [];
952: $fromIterable = false;
953: foreach ($array as $value) {
954: if (!is_iterable($value) || !$limit) {
955: $flattened[] = $value;
956: continue;
957: }
958: $fromIterable = true;
959: foreach ($value as $value) {
960: $flattened[] = $value;
961: }
962: }
963: $limit--;
964: } while ($fromIterable && $limit && ($array = $flattened));
965:
966: return $flattened;
967: }
968:
969: /**
970: * If a value is not an array, wrap it in one
971: *
972: * @template T
973: *
974: * @param T $value
975: * @return (T is null ? array{} : (T is array ? T : array{T}))
976: */
977: public static function wrap($value): array
978: {
979: if ($value === null) {
980: return [];
981: }
982: return is_array($value) ? $value : [$value];
983: }
984:
985: /**
986: * If a value is not a list, wrap it in one
987: *
988: * @template T
989: *
990: * @param T $value
991: * @return (T is null ? array{} : (T is list ? T : array{T}))
992: */
993: public static function wrapList($value): array
994: {
995: if ($value === null) {
996: return [];
997: }
998: return self::isList($value, true) ? $value : [$value];
999: }
1000:
1001: /**
1002: * Remove arrays wrapped around a value
1003: *
1004: * @param mixed $value
1005: * @param int $limit The maximum number of arrays to remove, or `-1` for no
1006: * limit.
1007: * @return mixed
1008: */
1009: public static function unwrap($value, int $limit = -1)
1010: {
1011: while (
1012: $limit
1013: && is_array($value)
1014: && count($value) === 1
1015: && array_key_first($value) === 0
1016: ) {
1017: $value = $value[0];
1018: $limit--;
1019: }
1020: return $value;
1021: }
1022:
1023: /**
1024: * @template T
1025: *
1026: * @param T $key
1027: * @return (T is array-key ? T : int)
1028: */
1029: private static function getKey($key, ?int &$i = null)
1030: {
1031: if (is_int($key)) {
1032: if ($i === null || $key > $i) {
1033: $i = $key;
1034: }
1035: return $key;
1036: }
1037: if (is_string($key)) {
1038: return $key;
1039: }
1040: if ($i === null) {
1041: return $i = 0;
1042: }
1043: return ++$i;
1044: }
1045: }
1046: