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