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