PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / vendor / vendor-prefixed / stellarwp / arrays / src / Arrays / Arr.php

Arr.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at vendor/vendor-prefixed/stellarwp/arrays/src/Arrays/Arr.php

1,477 lines 38.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Vendors\StellarWP\Arrays;
4
5 use ArrayAccess;
6 use Closure;
7 use InvalidArgumentException;
8
9 /**
10 * Array utilities
11 */
12 class Arr {
13 /**
14 * Determines if the given value is array accessible.
15 *
16 * @param mixed $value
17 *
18 * @return bool
19 */
20 public static function accessible( $value ): bool {
21 return is_array( $value ) || $value instanceof ArrayAccess;
22 }
23
24 /**
25 * Add an element to an array using "dot" notation if it doesn't exist.
26 *
27 * @param array $array
28 * @param string|int|float $key
29 * @param mixed $value
30 *
31 * @return array
32 */
33 public static function add( $array, $key, $value ) {
34 $key = explode( '.', $key );
35 $key = static::wrap( $key );
36 if ( is_null( static::get( $array, $key ) ) ) {
37 $array = static::set( $array, $key, $value );
38 }
39
40 return $array;
41 }
42
43 /**
44 * Duplicates any key not prefixed with '_' creating a prefixed duplicate one.
45 *
46 * The prefixing and duplication is recursive.
47 *
48 * @since 1.0.0
49 *
50 * @param mixed $array The array whose keys should be duplicated.
51 * @param bool $recursive Whether the prefixing and duplication should be
52 * recursive or shallow.
53 *
54 * @return array|mixed The array with the duplicate, prefixed, keys or the
55 * original input if not an array.
56 */
57 public static function add_prefixed_keys_to( $array, bool $recursive = false ) {
58 if ( ! is_array( $array ) ) {
59 return $array;
60 }
61
62 $prefixed = [];
63 foreach ( $array as $key => $value ) {
64 if ( $recursive && is_array( $value ) ) {
65 $value = self::add_prefixed_keys_to( $value, true );
66 // And also add it to the original array.
67 $array[ $key ] = array_merge( $array[ $key ], $value );
68 }
69
70 if ( 0 === strpos( $key, '_' ) ) {
71 continue;
72 }
73
74 $prefixed[ '_' . $key ] = $value;
75 }
76
77 return array_merge( $array, $prefixed );
78 }
79
80 /**
81 * Duplicates any key prefixed with '_' creating an un-prefixed duplicate one.
82 *
83 * The un-prefixing and duplication is recursive.
84 *
85 * @since 1.0.0
86 *
87 * @param mixed $array The array whose keys should be duplicated.
88 * @param bool $recursive Whether the un-prefixing and duplication should be
89 * recursive or shallow.
90 *
91 * @return mixed|array The array with the duplicate, unprefixed, keys or the
92 * original input if not an array.
93 */
94 public static function add_unprefixed_keys_to( $array, bool $recursive = false ) {
95 if ( ! is_array( $array ) ) {
96 return $array;
97 }
98
99 $unprefixed = [];
100 foreach ( $array as $key => $value ) {
101 if ( $recursive && is_array( $value ) ) {
102 $value = self::add_unprefixed_keys_to( $value, true );
103 // And also add it to the original array.
104 $array[ $key ] = array_merge( $array[ $key ], $value );
105 }
106
107 if ( 0 !== strpos( $key, '_' ) ) {
108 continue;
109 }
110 $unprefixed[ substr( $key, 1 ) ] = $value;
111 }
112
113 return array_merge( $array, $unprefixed );
114 }
115
116 /**
117 * Recursively visits all elements of an array applying the specified callback to each element
118 * key and value.
119 *
120 * @since 1.0.0
121 *
122 * @param array|mixed $input The input array whose nodes should be visited.
123 * @param callable $visitor A callback function that will be called on each array item; the callback will
124 * receive the item key and value as input and should return an array that contains
125 * the update key and value in the shape `[ <key>, <value> ]`. Returning a `null`
126 * key will cause the element to be removed from the array.
127 */
128 public static function array_visit_recursive( $input, callable $visitor ) {
129 if ( ! is_array( $input ) ) {
130 return $input;
131 }
132
133 $return = [];
134
135 foreach ( $input as $key => &$value ) {
136 if ( is_array( $value ) ) {
137 $value = static::array_visit_recursive( $value, $visitor );
138 }
139 // Ensure visitors can quickly return `null` to remove an element.
140 [ $updated_key, $update_value ] = array_replace( [ $key, $value ], static::wrap( $visitor( $key, $value ) ) );
141 if ( false === $updated_key ) {
142 // Visitor will be able to remove an element by returning a `false` key for it.
143 continue;
144 }
145 if ( null === $updated_key ) {
146 // Automatically assign the first available numeric index to the element.
147 $return[] = $update_value;
148 } else {
149 $return[ $updated_key ] = $update_value;
150 }
151 }
152
153 return $return;
154 }
155
156
157 /**
158 * Collapse an array of arrays into a single array.
159 *
160 * @param iterable $array
161 *
162 * @return array
163 */
164 public static function collapse( $array ) {
165 $results = [];
166
167 foreach ( $array as $values ) {
168 if ( ! is_array( $values ) ) {
169 continue;
170 }
171
172 $results[] = $values;
173 }
174
175 return array_merge( [], ...$results );
176 }
177
178 /**
179 * The inverse of the `stringify_keys` method, it will restore numeric keys for previously
180 * stringified keys.
181 *
182 * @since 1.0.0
183 *
184 * @param array<int|string,mixed> $input The input array whose stringified keys should be
185 * destringified.
186 * @param string $prefix The prefix that should be used to target only specific string keys.
187 *
188 * @return array<int|string,mixed> The input array, its stringified keys destringified.
189 */
190 public static function destringify_keys( array $input, string $prefix = 'sk_' ): array {
191 $visitor = static function( $key, $value ) use ( $prefix ) {
192 $destringified_key = 0 === self::strpos( $key, $prefix ) ? null : $key;
193
194 return [ $destringified_key, $value ];
195 };
196
197 return static::array_visit_recursive( $input, $visitor );
198 }
199
200
201 /**
202 * Flatten a multi-dimensional associative array with dots.
203 *
204 * @param iterable $array
205 * @param string $prepend
206 *
207 * @return array
208 */
209 public static function dot( $array, $prepend = '' ) {
210 $results = [];
211
212 foreach ( $array as $key => $value ) {
213 if ( is_array( $value ) ) {
214 $results = array_merge( $results, static::dot( $value, $prepend . $key . '.' ) );
215 } else {
216 $results[ $prepend . $key ] = $value;
217 }
218 }
219
220 return $results;
221 }
222
223 /**
224 * Sanitize a multidimensional array.
225 *
226 * @link https://gist.github.com/esthezia/5804445
227 * @since 1.0.0
228 *
229 * @param array|mixed $data The array to sanitize.
230 *
231 * @return array The sanitized array
232 *
233 */
234 public static function escape_multidimensional_array( $data = [] ): array {
235
236 if ( ! is_array( $data ) || ! count( $data ) ) {
237 return [];
238 }
239
240 foreach ( $data as $key => $value ) {
241 if ( ! is_array( $value ) && ! is_object( $value ) ) {
242 $data[ $key ] = esc_attr( trim( $value ) );
243 }
244 if ( is_array( $value ) ) {
245 $data[ $key ] = self::escape_multidimensional_array( $value );
246 }
247 }
248
249 return $data;
250 }
251
252 /**
253 * Discards everything other than array values having string keys and scalar values, ensuring a
254 * one-dimensional, associative array result.
255 *
256 * @link https://www.php.net/manual/language.types.array.php Keys cast to non-strings will be discarded.
257 *
258 * @since 1.0.0
259 *
260 * @param array|mixed $array
261 *
262 * @return array|mixed Associative or empty array.
263 */
264 public static function filter_to_flat_scalar_associative_array( $array ) {
265 $result = [];
266
267 if ( ! is_array( $array ) ) {
268 return $result;
269 }
270
271 foreach ( $array as $k => $v ) {
272 if ( ! is_string( $k ) ) {
273 continue;
274 }
275
276 if ( ! is_scalar( $v ) ) {
277 continue;
278 }
279
280 $result[ $k ] = $v;
281 }
282
283 return $result;
284 }
285
286 /**
287 * Get all of the given array except for a specified array of keys.
288 *
289 * @param array $array
290 * @param array|string|int|float $keys
291 *
292 * @return array
293 */
294 public static function except( $array, $keys ) {
295 $new_array = $array;
296 static::forget( $new_array, $keys );
297
298 return $new_array;
299 }
300
301
302 /**
303 * Determine if the given key exists in the provided array.
304 *
305 * @param \ArrayAccess|array $array
306 * @param string|int|float $key
307 *
308 * @return bool
309 */
310 public static function exists( $array, $key ) {
311 return static::has( $array, $key );
312 }
313
314 /**
315 * Filters an associative array non-recursively, keeping only the values attached
316 * to keys starting with the specified prefix.
317 *
318 * @since 1.0.0
319 *
320 * @param array $array The array to filter.
321 * @param string $prefix The prefix, or prefixes, of the keys to keep.
322 *
323 * @return array The filtered array.
324 */
325 public static function filter_prefixed( array $array, string $prefix ): array {
326 $prefixes = implode( '|', array_map( 'preg_quote', static::wrap( $prefix ) ) );
327 $pattern = '/^(' . $prefixes . ')/';
328 $filtered = [];
329 foreach ( $array as $key => $value ) {
330 if ( ! preg_match( $pattern, $key ) ) {
331 continue;
332 }
333 $filtered[ $key ] = $value;
334 }
335
336 return $filtered;
337 }
338
339 /**
340 * Return the first element in an array passing a given truth test.
341 *
342 * @param iterable $array
343 * @param callable|null $callback
344 * @param mixed $default
345 *
346 * @return mixed
347 */
348 public static function first( $array, ?callable $callback = null, $default = null ) {
349 if ( is_null( $callback ) ) {
350 if ( empty( $array ) ) {
351 return self::value( $default );
352 }
353
354 foreach ( $array as $item ) {
355 return $item;
356 }
357 }
358
359 foreach ( $array as $key => $value ) {
360 if ( $callback( $value, $key ) ) {
361 return $value;
362 }
363 }
364
365 return self::value( $default );
366 }
367
368 /**
369 * Flatten a multi-dimensional array into a single level.
370 *
371 * Typical use case is to flatten arrays like those returned by `get_post_meta( $id )`.
372 * Empty arrays are replaced with an empty string.
373 *
374 * @since 1.0.0
375 *
376 * @param iterable $array
377 * @param int $depth
378 *
379 * @return array The flattened array.
380 */
381 public static function flatten( $array, int $depth = PHP_INT_MAX ): array {
382 $result = [];
383
384 if ( $depth < 1 ) {
385 return $array;
386 }
387
388 foreach ( $array as $key => $item ) {
389 if ( ! is_array( $item ) ) {
390 // Preserve string keys, use numeric keys for numeric
391 if ( is_string( $key ) ) {
392 $result[ $key ] = $item;
393 } else {
394 $result[] = $item;
395 }
396 } else {
397 $values = $depth === 1
398 ? array_values( $item )
399 : static::flatten( $item, $depth - 1 );
400
401 foreach ( $values as $value_key => $value ) {
402 // Preserve string keys from nested arrays
403 if ( is_string( $value_key ) ) {
404 $result[ $value_key ] = $value;
405 } else {
406 $result[] = $value;
407 }
408 }
409 }
410 }
411
412 return $result;
413 }
414
415 /**
416 * Remove one or many array items from a given array using "dot" notation.
417 *
418 * @param array $array
419 * @param array|string|int|float $keys
420 *
421 * @return void
422 */
423 public static function forget( &$array, $keys ) {
424 $keys = (array) $keys;
425
426 foreach ( $keys as $key ) {
427 // Convert dot notation to array segments
428 $parts = explode( '.', $key );
429
430 if ( count( $parts ) === 1 ) {
431 unset( $array[ $key ] );
432 continue;
433 }
434
435 // For nested keys, traverse the array
436 $current = &$array;
437 $lastKey = array_pop( $parts );
438
439 foreach ( $parts as $part ) {
440 if ( ! isset( $current[ $part ] ) || ! is_array( $current[ $part ] ) ) {
441 continue 2;
442 }
443 $current = &$current[ $part ];
444 }
445
446 unset( $current[ $lastKey ] );
447 }
448 }
449
450 /**
451 * Find a value inside of an array or object, including one nested a few levels deep.
452 *
453 * Example: get( $a, [ 0, 1, 2 ] ) returns the value of $a[0][1][2] or the default.
454 *
455 * @param array|object|mixed $variable Array or object to search within.
456 * @param array|string|int|null $indexes Specify each nested index in order. Can also be in dot notation.
457 * Example: array( 'lvl1', 'lvl2' ) or 'lvl1.lvl2'.
458 * @param mixed $default Default value if the search finds nothing.
459 *
460 * @return mixed The value of the specified index or the default if not found.
461 * @throws \InvalidArgumentException If the provided variable is not an array and does not implement ArrayAccess.
462 */
463 public static function get( $variable, $indexes, $default = null ) {
464 if ( ! static::accessible( $variable ) ) {
465 throw new \InvalidArgumentException( 'The provided variable is not an array and does not implement ArrayAccess.' );
466 }
467
468 if ( is_null( $indexes ) ) {
469 return $variable;
470 }
471
472 if ( is_string( $indexes ) && isset( $variable[ $indexes ] ) ) {
473 return $variable[ $indexes ];
474 } elseif ( is_string( $indexes ) ) {
475 $indexes = explode( '.', $indexes );
476 }
477
478 $indexes = static::wrap( $indexes );
479
480 foreach ( $indexes as $index ) {
481 if ( ! static::exists( $variable, $index ) ) {
482 $variable = $default;
483 break;
484 }
485
486 $variable = $variable[ $index ];
487 }
488
489 return $variable;
490 }
491
492 /**
493 * Returns the value associated with the first index, among the indexes, that is set in the array..
494 *
495 * @since 1.0.0
496 *
497 * @param array $array The array to search.
498 * @param array $indexes The indexes to search; in order the function will look from the first to the last.
499 * @param mixed $default The value that will be returned if the array does not have any of the indexes set.
500 *
501 * @return mixed|null The set value or the default value.
502 */
503 public static function get_first_set( array $array, array $indexes, $default = null ) {
504 foreach ( $indexes as $index ) {
505 if ( ! isset( $array[ $index ] ) ) {
506 continue;
507 }
508
509 return $array[ $index ];
510 }
511
512 return $default;
513 }
514
515 /**
516 * Find a value inside a list of array or objects, including one nested a few levels deep.
517 *
518 * @since 1.0.0
519 *
520 * Example: get( [$a, $b, $c], [ 0, 1, 2 ] ) returns the value of $a[0][1][2] found in $a, $b or $c
521 * or the default.
522 *
523 * @param array $variables Array of arrays or objects to search within.
524 * @param array|string $indexes Specify each nested index in order.
525 * Example: array( 'lvl1', 'lvl2' );
526 * @param mixed $default Default value if the search finds nothing.
527 *
528 * @return mixed The value of the specified index or the default if not found.
529 */
530 public static function get_in_any( array $variables, $indexes, $default = null ) {
531 foreach ( $variables as $variable ) {
532 $found = self::get( $variable, $indexes, '__not_found__' );
533 if ( '__not_found__' !== $found ) {
534 return $found;
535 }
536 }
537
538 return $default;
539 }
540
541
542 /**
543 * Check if an item or items exist in an array using "dot" notation.
544 *
545 * @param \ArrayAccess|array $array
546 * @param array|string|int|null $indexes The indexes to search; in order the function will look from the first to the last.
547 *
548 * @return bool
549 */
550 public static function has( $array, $indexes ) {
551 if ( ! is_array( $array ) ) {
552 return false;
553 }
554
555 if ( isset( $array[ $indexes ] ) ) {
556 return true;
557 }
558
559 if ( is_string( $indexes ) && isset( $array[ $indexes ] ) ) {
560 return true;
561 }
562
563 if ( is_string( $indexes ) ) {
564 $indexes = explode( '.', $indexes );
565 }
566
567 // Start with the root array
568 $current = &$array;
569
570 // Get all segments except the last one
571 $segments = array_slice( $indexes, 0, -1 );
572 $final_key = end( $indexes );
573
574 // Iterate through every key, setting the pointer one level deeper each time.
575 foreach ( $segments as $segment ) {
576 if ( ! isset( $current[ $segment ] ) || ! is_array( $current[ $segment ] ) ) {
577 return false;
578 }
579 $current = &$current[ $segment ];
580 }
581
582 return isset( $current[ $final_key ] );
583 }
584
585 /**
586 * Insert an array after a specified key within another array.
587 *
588 * @param string|int $key The key of the array to insert after.
589 * @param array $source_array The array to insert into.
590 * @param mixed $insert Value or array to insert.
591 *
592 * @return array
593 */
594 public static function insert_after_key( $key, array $source_array, $insert ): array {
595 if ( ! is_array( $insert ) ) {
596 $insert = [ $insert ];
597 }
598
599 if ( array_key_exists( $key, $source_array ) ) {
600 $position = array_search( $key, array_keys( $source_array ) ) + 1;
601 $source_array = array_slice( $source_array, 0, $position, true ) + $insert + array_slice( $source_array, $position, null, true );
602 } else {
603 // If no key is found, then add it to the end of the array.
604 $source_array += $insert;
605 }
606
607 return $source_array;
608 }
609
610 /**
611 * Insert an array immediately before a specified key within another array.
612 *
613 * @param string|int $key The key of the array to insert before.
614 * @param array $source_array The array to insert into.
615 * @param mixed $insert Value or array to insert.
616 *
617 * @return array
618 */
619 public static function insert_before_key( $key, array $source_array, $insert ): array {
620 if ( ! is_array( $insert ) ) {
621 $insert = [ $insert ];
622 }
623
624 if ( array_key_exists( $key, $source_array ) ) {
625 $position = array_search( $key, array_keys( $source_array ) );
626 $source_array = array_slice( $source_array, 0, $position, true ) + $insert + array_slice( $source_array, $position, null, true );
627 } else {
628 // If no key is found, then add it to the end of the array.
629 $source_array += $insert;
630 }
631
632 return $source_array;
633 }
634
635
636 /**
637 * Determines if an array is associative.
638 *
639 * An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
640 *
641 * @param array $array
642 *
643 * @return bool
644 */
645 public static function is_assoc( array $array ) {
646 return ! static::is_list( $array );
647 }
648
649 /**
650 * Determines if an array is a list.
651 *
652 * An array is a "list" if all array keys are sequential integers starting from 0 with no gaps in between.
653 *
654 * @param array $array
655 *
656 * @return bool
657 */
658 public static function is_list( $array ) {
659 if ( function_exists( 'array_is_list' ) ) {
660 return array_is_list( $array );
661 }
662
663 $i = 0;
664 foreach ( $array as $k => $v ) {
665 if ( $k !== $i++ ) {
666 return false;
667 }
668 }
669
670 return true;
671 }
672
673
674 /**
675 * Join all items using a string. The final items can use a separate glue string.
676 *
677 * @param array $array
678 * @param string $glue
679 * @param string $finalGlue
680 *
681 * @return string
682 */
683 public static function join( $array, $glue, $finalGlue = '' ) {
684 if ( $finalGlue === '' ) {
685 return implode( $glue, $array );
686 }
687
688 if ( count( $array ) === 0 ) {
689 return '';
690 }
691
692 if ( count( $array ) === 1 ) {
693 return end( $array );
694 }
695
696 $finalItem = array_pop( $array );
697
698 return implode( $glue, $array ) . $finalGlue . $finalItem;
699 }
700
701 /**
702 * Return the last element in an array passing a given truth test.
703 *
704 * @param array $array
705 * @param callable|null $callback
706 * @param mixed $default
707 *
708 * @return mixed
709 */
710 public static function last( $array, ?callable $callback = null, $default = null ) {
711 if ( is_null( $callback ) ) {
712 return empty( $array ) ? self::value( $default ) : end( $array );
713 }
714
715 return static::first( array_reverse( $array, true ), $callback, $default );
716 }
717
718 /**
719 * Converts a list to an array filtering out empty string elements.
720 *
721 * @param string|mixed|null $value A string representing a list of values separated by the specified separator
722 * or an array. If the list is a string (e.g. a CSV list) then it will urldecoded
723 * before processing.
724 * @param string|mixed $sep The char(s) separating the list elements; will be ignored if the list is an array.
725 *
726 * @return array An array of list elements.
727 */
728 public static function list_to_array( $value, $sep = ',' ): array {
729 // Let's not jump through all the hoops if the value is empty.
730 if ( empty( $value ) ) {
731 return [];
732 }
733 // since we might receive URL encoded strings for CSV lists let's URL decode them first
734 $value = is_array( $value ) ? $value : urldecode( $value );
735
736 $sep = ! is_string( $sep ) ? ',' : $sep;
737
738 if ( $value === '' ) {
739 return [];
740 }
741
742 if ( ! is_array( $value ) ) {
743 $value = preg_split( '/\\s*' . preg_quote( $sep, '/' ) . '\\s*/', $value );
744 }
745
746 $filtered = [];
747 foreach ( $value as $v ) {
748 if ( '' === $v ) {
749 continue;
750 }
751 $filtered[] = is_numeric( $v ) ? $v + 0 : $v;
752 }
753
754 return $filtered;
755 }
756
757 /**
758 * Returns an array of values obtained by using the keys on the map; keys
759 * that do not have a match in map are discarded.
760 *
761 * To discriminate from not found results and legitimately `false`
762 * values from the map the `$found` parameter will be set by reference.
763 *
764 * @since 1.0.0
765 *
766 * @param string|array $keys One or more keys that should be used to get
767 * the new values
768 * @param array $map An associative array relating the keys to the new
769 * values.
770 * @param bool $found When using a single key this argument will be
771 * set to indicate whether the mapping was successful
772 * or not.
773 *
774 * @return array|mixed|false An array of mapped values, a single mapped value when passing
775 * one key only or `false` if one key was passed but the key could
776 * not be mapped.
777 */
778 public static function map_or_discard( $keys, array $map, bool &$found = true ) {
779 $hash = md5( (string) time() );
780 $mapped = [];
781
782 foreach ( (array) $keys as $key ) {
783 $meta_key = self::get( $map, $key, $hash );
784 if ( $hash === $meta_key ) {
785 continue;
786 }
787 $mapped[] = $meta_key;
788 }
789
790 $found = (bool) count( $mapped );
791
792 if ( is_array( $keys ) ) {
793 return $mapped;
794 }
795
796 return $found ? $mapped[0] : false;
797 }
798
799 /**
800 * Recursively merge two arrays preserving keys.
801 *
802 * @link http://php.net/manual/en/function.array-merge-recursive.php#92195
803 *
804 * @since 1.0.0
805 *
806 * @param array $array1
807 * @param array $array2
808 *
809 * @return array
810 */
811 public static function merge_recursive( array &$array1, array &$array2 ): array {
812 $merged = $array1;
813
814 foreach ( $array2 as $key => &$value ) {
815 if ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
816 $merged[ $key ] = static::merge_recursive( $merged[ $key ], $value );
817 } else if ( is_int( $key ) ) {
818 $merged[] = $value;
819 } else {
820 $merged[ $key ] = $value;
821 }
822 }
823
824 return $merged;
825 }
826
827 /**
828 * Merges two or more arrays in the nested format used by WP_Query arguments preserving and merging them correctly.
829 *
830 * The method will recursively replace named keys and merge numeric keys. The method takes its name from its intended
831 * primary use, but it's not limited to query arguments only.
832 *
833 * @since 1.0.0
834 *
835 * @param array<string|int,mixed> ...$arrays A set of arrays to merge.
836 *
837 * @return array<string|int,mixed> The recursively merged array.
838 */
839 public static function merge_recursive_query_vars( array ...$arrays ): array {
840 if ( ! count( $arrays ) ) {
841 return [];
842 }
843
844 // Temporarily transform numeric keys to string keys generated with time-related randomness.
845 $stringified = array_map( [ static::class, 'stringify_keys' ], $arrays );
846 // Replace recursive will recursively replace any entry that has the same string key, stringified keys will never match due to randomness.
847 $merged = array_replace_recursive( ...$stringified );
848
849 // Finally destringify the keys to return something that will resemble, in shape, the original arrays.
850 return static::destringify_keys( $merged );
851 }
852
853 /**
854 * Get a subset of the items from the given array.
855 *
856 * @param array $array
857 * @param array|string $keys
858 *
859 * @return array
860 */
861 public static function only( $array, $keys ) {
862 return array_intersect_key( $array, array_flip( static::wrap( $keys ) ) );
863 }
864
865 /**
866 * Build an array from migrating aliased key values to their canonical key values, removing all alias keys.
867 *
868 * If the original array has values for both the alias and its canonical, keep the canonical's value and
869 * discard the alias' value.
870 *
871 * @since 1.0.0
872 *
873 * @param array $original An associative array of values, such as passed shortcode arguments.
874 * @param array $alias_map An associative array of aliases: key as alias, value as mapped canonical.
875 * Example: [ 'alias' => 'canonical', 'from' => 'to', 'that' => 'becomes_this' ]
876 *
877 * @return array
878 */
879 public static function parse_associative_array_alias( array $original, array $alias_map ): array {
880 // Ensure array values.
881 $alias_map = static::filter_to_flat_scalar_associative_array( $alias_map );
882
883 // Fail gracefully if alias array wasn't setup as [ 'from' => 'to' ].
884 if ( empty( $alias_map ) ) {
885 return $original;
886 }
887
888 $result = $original;
889
890 // Parse aliases.
891 foreach ( $alias_map as $from => $to ) {
892 // If this alias isn't in use, go onto the next.
893 if ( ! isset( $result[ $from ] ) ) {
894 continue;
895 }
896
897 // Only allow setting alias value if canonical value is not already present.
898 if ( ! isset( $result[ $to ] ) ) {
899 $result[ $to ] = $result[ $from ];
900 }
901
902 // Always remove the alias key.
903 unset( $result[ $from ] );
904 }
905
906 return $result;
907 }
908
909 /**
910 * Push an item onto the beginning of an array.
911 *
912 * @param array $array
913 * @param mixed $value
914 * @param mixed $key
915 *
916 * @return array
917 */
918 public static function prepend( $array, $value, $key = null ) {
919 if ( func_num_args() == 2 ) {
920 array_unshift( $array, $value );
921 } else {
922 $array = [ $key => $value ] + $array;
923 }
924
925 return $array;
926 }
927
928 /**
929 * Get a value from the array, and remove it.
930 *
931 * @param array $array
932 * @param string|int $key
933 * @param mixed $default
934 *
935 * @return mixed
936 */
937 public static function pull( &$array, $key, $default = null ) {
938 $value = static::get( $array, $key, $default );
939
940 static::forget( $array, $key );
941
942 return $value;
943 }
944
945 /**
946 * Convert the array into a query string.
947 *
948 * @param array $array
949 *
950 * @return string
951 */
952 public static function query( $array ) {
953 return http_build_query( $array, '', '&', PHP_QUERY_RFC3986 );
954 }
955
956 /**
957 * Get one or a specified number of random values from an array.
958 *
959 * @param array $array
960 * @param int|null $number
961 * @param bool $preserveKeys
962 *
963 * @return mixed
964 *
965 * @throws \InvalidArgumentException
966 */
967 public static function random( $array, $number = null, $preserveKeys = false ) {
968 $requested = is_null( $number ) ? 1 : $number;
969
970 $count = count( $array );
971
972 if ( $requested > $count ) {
973 throw new InvalidArgumentException(
974 "You requested {$requested} items, but there are only {$count} items available."
975 );
976 }
977
978 if ( is_null( $number ) ) {
979 return $array[ array_rand( $array ) ];
980 }
981
982 if ( (int) $number === 0 ) {
983 return [];
984 }
985
986 $keys = array_rand( $array, $number );
987 $keys = static::wrap( $keys );
988
989 $results = [];
990
991 if ( $preserveKeys ) {
992 foreach ( $keys as $key ) {
993 $results[ $key ] = $array[ $key ];
994 }
995 } else {
996 foreach ( $keys as $key ) {
997 $results[] = $array[ $key ];
998 }
999 }
1000
1001 return $results;
1002 }
1003
1004 /**
1005 * Recursively key-sort an array.
1006 *
1007 * @since 1.0.0
1008 *
1009 * @param array $array The array to sort, modified by reference.
1010 *
1011 * @return bool The sorting result.
1012 */
1013 public static function recursive_ksort( array &$array ): bool {
1014 // First recursively sort all sub-arrays
1015 foreach ( $array as &$value ) {
1016 if ( is_array( $value ) ) {
1017 static::recursive_ksort( $value );
1018 }
1019 }
1020
1021 // Then sort the current array, ensuring numeric keys are sorted numerically
1022 return ksort( $array, SORT_NATURAL );
1023 }
1024
1025 /**
1026 * Recursively remove numeric keys from an array.
1027 *
1028 * @since 1.0.0
1029 *
1030 * @param array<string|int,mixed> $input The input array.
1031 *
1032 * @return array<int|mixed> An array that only contains integer keys at any of its levels.
1033 */
1034 public static function remove_numeric_keys_recursive( array $input ): array {
1035 return self::array_visit_recursive(
1036 $input,
1037 static function( $key ) {
1038 return is_numeric( $key ) ? false : $key;
1039 }
1040 );
1041 }
1042
1043 /**
1044 * Recursively remove numeric keys from an array.
1045 *
1046 * @since 1.0.0
1047 *
1048 * @param array<string|int,mixed> $input The input array.
1049 *
1050 * @return array<string,mixed> An array that only contains non numeric keys at any of its levels.
1051 */
1052 public static function remove_string_keys_recursive( array $input ): array {
1053 return self::array_visit_recursive(
1054 $input,
1055 static function( $key ) {
1056 return ! is_numeric( $key ) ? false : $key;
1057 }
1058 );
1059 }
1060
1061 /**
1062 * Set key/value within an array, can set a key nested inside of a multidimensional array.
1063 *
1064 * Example: set( $a, [ 0, 1, 2 ], 'hi' ) sets $a[0][1][2] = 'hi' and returns $a.
1065 *
1066 * @param mixed $array The array containing the key this sets.
1067 * @param string|array $key To set a key nested multiple levels deep pass an array
1068 * specifying each key in order as a value.
1069 * Example: array( 'lvl1', 'lvl2', 'lvl3' );
1070 * @param mixed $value The value.
1071 *
1072 * @return array Full array with the key set to the specified value.
1073 */
1074 public static function set( $array, $key, $value ): array {
1075 // Convert input to array if not already
1076 if ( ! is_array( $array ) ) {
1077 $array = [];
1078 }
1079
1080 // If key is a string with dots, convert to array
1081 if ( is_string( $key ) ) {
1082 $key = explode( '.', $key );
1083 }
1084
1085 // Convert strings and such to array.
1086 $key = static::wrap( $key );
1087
1088 // Start with the root array
1089 $current = &$array;
1090
1091 // Get all segments except the last one
1092 $segments = array_slice( $key, 0, -1 );
1093 $final_key = end( $key );
1094
1095 // Traverse through each key segment
1096 foreach ( $segments as $segment ) {
1097 // If the current segment is a Closure or not an array, convert it to an array
1098 if ( ! isset( $current[ $segment ] ) || ! is_array( $current[ $segment ] ) ) {
1099 $current[ $segment ] = [];
1100 }
1101
1102 // Move pointer to next level
1103 $current = &$current[ $segment ];
1104 }
1105
1106 // Set the final value
1107 $current[ $final_key ] = $value;
1108
1109 return $array;
1110 }
1111
1112 /**
1113 * Shapes, filtering it, an array to the specified expected set of required keys.
1114 *
1115 * @since 1.0.0
1116 *
1117 * @param array $array The input array to shape.
1118 * @param array $shape The shape to update the array with. It should only define keys
1119 * or arrays of keys. Keys that have no values will be set to `null`.
1120 * To add the key only if set, prefix the key with `?`, e.g. `?foo`.
1121 *
1122 * @return array The input array shaped and ordered per the shape.
1123 */
1124 public static function shape_filter( array $array, array $shape ): array {
1125 $shaped = [];
1126 foreach ( $shape as $shape_index => $shape_key ) {
1127 $optional = is_array( $shape_key ) ?
1128 strpos( $shape_index, '?' ) === 0
1129 : strpos( $shape_key, '?' ) === 0;
1130
1131 if ( is_array( $shape_key ) ) {
1132 $shape_index = $optional ? substr( $shape_index, 1 ) : $shape_index;
1133 if ( $optional && ! isset( $array[ $shape_index ] ) ) {
1134 continue;
1135 }
1136 $shaped[ $shape_index ] = self::shape_filter( $array[ $shape_index ] ?? [], $shape_key );
1137 } else {
1138 $shape_key = $optional ? substr( $shape_key, 1 ) : $shape_key;
1139 if ( ! isset( $array[ $shape_key ] ) && $optional ) {
1140 continue;
1141 }
1142 $shaped[ $shape_key ] = $array[ $shape_key ] ?? null;
1143 }
1144 }
1145
1146 return $shaped;
1147 }
1148
1149 /**
1150 * Shuffle the given array and return the result.
1151 *
1152 * @param array $array
1153 * @param int|null $seed
1154 *
1155 * @return array
1156 */
1157 public static function shuffle( $array, $seed = null ) {
1158 if ( is_null( $seed ) ) {
1159 shuffle( $array );
1160 } else {
1161 mt_srand( $seed );
1162 shuffle( $array );
1163 mt_srand();
1164 }
1165
1166 return $array;
1167 }
1168
1169 /**
1170 * Sort based on Priority
1171 *
1172 * @since 1.0.0
1173 *
1174 * @param array $array Array to sort.
1175 *
1176 * @return array
1177 */
1178 public static function sort_by_priority( $array ): array {
1179 if ( ! is_array( $array ) ) {
1180 throw new \InvalidArgumentException( 'Input must be an array' );
1181 }
1182
1183 if ( static::is_assoc( $array ) ) {
1184 uasort( $array, [ static::class, 'sort_by_priority_comparison' ] );
1185 } else {
1186 usort( $array, [ static::class, 'sort_by_priority_comparison' ] );
1187 }
1188
1189 return $array;
1190 }
1191
1192 /**
1193 * Sort based on Priority
1194 *
1195 * @since 1.0.0
1196 *
1197 * @param object|array $b Second subject to compare
1198 *
1199 * @param object|array $a First Subject to compare
1200 *
1201 * @return int
1202 */
1203 public static function sort_by_priority_comparison( $a, $b ): int {
1204 if ( is_array( $a ) ) {
1205 $a_priority = $a['priority'];
1206 } else {
1207 $a_priority = $a->priority;
1208 }
1209
1210 if ( is_array( $b ) ) {
1211 $b_priority = $b['priority'];
1212 } else {
1213 $b_priority = $b->priority;
1214 }
1215
1216 if ( (int) $a_priority === (int) $b_priority ) {
1217 return 0;
1218 }
1219
1220 return (int) $a_priority > (int) $b_priority ? 1 : -1;
1221 }
1222
1223 /**
1224 * Recursively sort an array by keys and values.
1225 *
1226 * @param array $array
1227 * @param int $options
1228 * @param bool $descending
1229 *
1230 * @return array
1231 */
1232 public static function sort_recursive( $array, $options = SORT_REGULAR, $descending = false ) {
1233 foreach ( $array as &$value ) {
1234 if ( is_array( $value ) ) {
1235 $value = static::sort_recursive( $value, $options, $descending );
1236 }
1237 }
1238
1239 if ( ! static::is_list( $array ) ) {
1240 $descending
1241 ? krsort( $array, $options )
1242 : ksort( $array, $options );
1243 } else {
1244 $descending
1245 ? rsort( $array, $options )
1246 : sort( $array, $options );
1247 }
1248
1249 return $array;
1250 }
1251
1252 /**
1253 * Recursively sort an array by keys and values in descending order.
1254 *
1255 * @param array $array
1256 * @param int $options
1257 *
1258 * @return array
1259 */
1260 public static function sort_recursive_desc( $array, $options = SORT_REGULAR ) {
1261 return static::sort_recursive( $array, $options, true );
1262 }
1263
1264 /**
1265 * Stringifies the numeric keys of an array.
1266 *
1267 * @since 1.0.0
1268 *
1269 * @param array<int|string,mixed> $input The input array whose keys should be stringified.
1270 * @param string|null $prefix The prefix that should be use to stringify the keys, if not provided
1271 * then it will be generated.
1272 *
1273 * @return array<string,mixed> The input array with each numeric key stringified.
1274 */
1275 public static function stringify_keys( array $input, $prefix = null ): array {
1276 $prefix = null === $prefix ? uniqid( 'sk_' ) : $prefix;
1277 $visitor = static function( $key, $value ) use ( $prefix ) {
1278 $string_key = is_numeric( $key ) ? $prefix . $key : $key;
1279
1280 return [ $string_key, $value ];
1281 };
1282
1283 return static::array_visit_recursive( $input, $visitor );
1284 }
1285
1286 /**
1287 * Behaves exactly like the native strpos(), but accepts an array of needles.
1288 *
1289 * @see strpos()
1290 *
1291 * @param string $haystack String to search in.
1292 * @param array|string $needles Strings to search for.
1293 * @param int $offset Starting position of search.
1294 *
1295 * @return false|int Integer position of first needle occurrence.
1296 */
1297 public static function strpos( string $haystack, $needles, int $offset = 0 ) {
1298 $needles = static::wrap( $needles );
1299
1300 $needles = array_filter( $needles, static function( $needle ) {
1301 return is_string( $needle ) && $needle !== '';
1302 } );
1303
1304 foreach ( $needles as $i ) {
1305 $search = strpos( $haystack, $i, $offset );
1306
1307 if ( false !== $search ) {
1308 return $search;
1309 }
1310 }
1311
1312 return false;
1313 }
1314
1315 /**
1316 * Returns a list separated by the specified separator.
1317 *
1318 * @since 1.0.0
1319 *
1320 * @param mixed $list
1321 * @param string $sep
1322 *
1323 * @return string The list separated by the specified separator or the original list if the list is empty.
1324 */
1325 public static function to_list( $list, string $sep = ',' ): string {
1326 if ( $list === null ) {
1327 return '';
1328 }
1329
1330 if ( empty( $list ) ) {
1331 return is_array( $list ) ? '' : $list;
1332 }
1333
1334 if ( is_array( $list ) ) {
1335 return implode( $sep, $list );
1336 }
1337
1338 return $list;
1339 }
1340
1341 /**
1342 * Convert a flatten "dot" notation array into an expanded array.
1343 *
1344 * @param iterable $array
1345 *
1346 * @return array
1347 */
1348 public static function undot( $array ) {
1349 $results = [];
1350
1351 foreach ( $array as $key => $value ) {
1352 $results = static::set( $results, explode( '.', $key ), $value );
1353 }
1354
1355 return $results;
1356 }
1357
1358 /**
1359 * Searches an array using a callback and returns the index of the first match.
1360 *
1361 * This method fills the gap left by the non-existence of an `array_usearch` function.
1362 *
1363 * @since 1.0.0
1364 *
1365 * @param mixed $needle The element to search in the array.
1366 * @param array $haystack The array to search.
1367 * @param callable $callback A callback function with signature `fn($needle, $value, $key) :bool`
1368 * that will be used to find the first match of needle in haystack.
1369 *
1370 * @return string|int|false Either the index of the first match or `false` if no match was found.
1371 */
1372 public static function usearch( $needle, array $haystack, callable $callback ) {
1373 foreach ( $haystack as $key => $value ) {
1374 if ( $callback( $needle, $value, $key ) ) {
1375 return $key;
1376 }
1377 }
1378
1379 return false;
1380 }
1381
1382 /**
1383 * Filter the array using the given callback.
1384 *
1385 * @param array $array
1386 * @param callable $callback
1387 *
1388 * @return array
1389 */
1390 public static function where( $array, callable $callback ) {
1391 return array_filter( $array, $callback, ARRAY_FILTER_USE_BOTH );
1392 }
1393
1394 /**
1395 * Filter items where the value is not null.
1396 *
1397 * @param array $array
1398 *
1399 * @return array
1400 */
1401 public static function where_not_null( $array ) {
1402 return static::where( $array, static function( $value ) {
1403 return ! is_null( $value );
1404 } );
1405 }
1406
1407 /**
1408 * If the given value is not an array and not null, wrap it in one.
1409 *
1410 * @param mixed $value
1411 *
1412 * @return array
1413 */
1414 public static function wrap( $value ) {
1415 if ( is_null( $value ) ) {
1416 return [];
1417 }
1418
1419 return is_array( $value ) ? $value : [ $value ];
1420 }
1421
1422 /**
1423 * Recursively computes the intersection of arrays using keys for comparison.
1424 *
1425 * @param mixed[] $array The array with master keys to check.
1426 * @param mixed[] ...$arrays Additional arrays to compare keys against.
1427 *
1428 * @return mixed[] An associative array containing all the entries of $array
1429 * whose keys exist in every provided array, recursively.
1430 */
1431 public static function intersect_key_recursive( array $array, array ...$arrays ): array {
1432 $array = array_intersect_key( $array, ...$arrays );
1433
1434 foreach ( $array as $key => $value ) {
1435 if ( ! is_array( $value ) ) {
1436 continue;
1437 }
1438
1439 $arrays_to_intersect = [];
1440 foreach ( $arrays as $intersect_array ) {
1441 if ( ! isset( $intersect_array[ $key ] ) ) {
1442 unset( $array[ $key ] );
1443 continue 2;
1444 }
1445
1446 if ( ! is_array( $intersect_array[ $key ] ) ) {
1447 continue;
1448 }
1449
1450 $arrays_to_intersect[] = $intersect_array[ $key ];
1451 }
1452
1453 if ( empty( $arrays_to_intersect ) ) {
1454 continue;
1455 }
1456
1457 $array[ $key ] = static::intersect_key_recursive( $value, ...$arrays_to_intersect );
1458 }
1459
1460 return $array;
1461 }
1462
1463 /**
1464 * Return the default value of the given value.
1465 *
1466 * @template TValue
1467 * @template TArgs
1468 *
1469 * @param TValue|\Closure(TArgs): TValue $value
1470 * @param TArgs ...$args
1471 * @return TValue
1472 */
1473 private static function value( $value, ...$args ) {
1474 return $value instanceof Closure ? $value( ...$args ) : $value;
1475 }
1476 }
1477