blob: b99db01fed5e3275fd3c09965680ccb21616ca38 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Tightenco\Collect\Support;
4
5use ArrayAccess;
6use Tightenco\Collect\Support\Traits\Macroable;
7use InvalidArgumentException;
8
9class Arr
10{
11 use Macroable;
12
13 /**
14 * Determine whether the given value is array accessible.
15 *
16 * @param mixed $value
17 * @return bool
18 */
19 public static function accessible($value)
20 {
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 $key
29 * @param mixed $value
30 * @return array
31 */
32 public static function add($array, $key, $value)
33 {
34 if (is_null(static::get($array, $key))) {
35 static::set($array, $key, $value);
36 }
37
38 return $array;
39 }
40
41 /**
42 * Collapse an array of arrays into a single array.
43 *
44 * @param iterable $array
45 * @return array
46 */
47 public static function collapse($array)
48 {
49 $results = [];
50
51 foreach ($array as $values) {
52 if ($values instanceof Collection) {
53 $values = $values->all();
54 } elseif (! is_array($values)) {
55 continue;
56 }
57
58 $results[] = $values;
59 }
60
61 return array_merge([], ...$results);
62 }
63
64 /**
65 * Cross join the given arrays, returning all possible permutations.
66 *
67 * @param iterable ...$arrays
68 * @return array
69 */
70 public static function crossJoin(...$arrays)
71 {
72 $results = [[]];
73
74 foreach ($arrays as $index => $array) {
75 $append = [];
76
77 foreach ($results as $product) {
78 foreach ($array as $item) {
79 $product[$index] = $item;
80
81 $append[] = $product;
82 }
83 }
84
85 $results = $append;
86 }
87
88 return $results;
89 }
90
91 /**
92 * Divide an array into two arrays. One with keys and the other with values.
93 *
94 * @param array $array
95 * @return array
96 */
97 public static function divide($array)
98 {
99 return [array_keys($array), array_values($array)];
100 }
101
102 /**
103 * Flatten a multi-dimensional associative array with dots.
104 *
105 * @param iterable $array
106 * @param string $prepend
107 * @return array
108 */
109 public static function dot($array, $prepend = '')
110 {
111 $results = [];
112
113 foreach ($array as $key => $value) {
114 if (is_array($value) && ! empty($value)) {
115 $results = array_merge($results, static::dot($value, $prepend.$key.'.'));
116 } else {
117 $results[$prepend.$key] = $value;
118 }
119 }
120
121 return $results;
122 }
123
124 /**
125 * Get all of the given array except for a specified array of keys.
126 *
127 * @param array $array
128 * @param array|string $keys
129 * @return array
130 */
131 public static function except($array, $keys)
132 {
133 static::forget($array, $keys);
134
135 return $array;
136 }
137
138 /**
139 * Determine if the given key exists in the provided array.
140 *
141 * @param \ArrayAccess|array $array
142 * @param string|int $key
143 * @return bool
144 */
145 public static function exists($array, $key)
146 {
147 if ($array instanceof Enumerable) {
148 return $array->has($key);
149 }
150
151 if ($array instanceof ArrayAccess) {
152 return $array->offsetExists($key);
153 }
154
155 return array_key_exists($key, $array);
156 }
157
158 /**
159 * Return the first element in an array passing a given truth test.
160 *
161 * @param iterable $array
162 * @param callable|null $callback
163 * @param mixed $default
164 * @return mixed
165 */
166 public static function first($array, callable $callback = null, $default = null)
167 {
168 if (is_null($callback)) {
169 if (empty($array)) {
170 return value($default);
171 }
172
173 foreach ($array as $item) {
174 return $item;
175 }
176 }
177
178 foreach ($array as $key => $value) {
179 if ($callback($value, $key)) {
180 return $value;
181 }
182 }
183
184 return value($default);
185 }
186
187 /**
188 * Return the last element in an array passing a given truth test.
189 *
190 * @param array $array
191 * @param callable|null $callback
192 * @param mixed $default
193 * @return mixed
194 */
195 public static function last($array, callable $callback = null, $default = null)
196 {
197 if (is_null($callback)) {
198 return empty($array) ? value($default) : end($array);
199 }
200
201 return static::first(array_reverse($array, true), $callback, $default);
202 }
203
204 /**
205 * Flatten a multi-dimensional array into a single level.
206 *
207 * @param iterable $array
208 * @param int $depth
209 * @return array
210 */
211 public static function flatten($array, $depth = INF)
212 {
213 $result = [];
214
215 foreach ($array as $item) {
216 $item = $item instanceof Collection ? $item->all() : $item;
217
218 if (! is_array($item)) {
219 $result[] = $item;
220 } else {
221 $values = $depth === 1
222 ? array_values($item)
223 : static::flatten($item, $depth - 1);
224
225 foreach ($values as $value) {
226 $result[] = $value;
227 }
228 }
229 }
230
231 return $result;
232 }
233
234 /**
235 * Remove one or many array items from a given array using "dot" notation.
236 *
237 * @param array $array
238 * @param array|string $keys
239 * @return void
240 */
241 public static function forget(&$array, $keys)
242 {
243 $original = &$array;
244
245 $keys = (array) $keys;
246
247 if (count($keys) === 0) {
248 return;
249 }
250
251 foreach ($keys as $key) {
252 // if the exact key exists in the top-level, remove it
253 if (static::exists($array, $key)) {
254 unset($array[$key]);
255
256 continue;
257 }
258
259 $parts = explode('.', $key);
260
261 // clean up before each pass
262 $array = &$original;
263
264 while (count($parts) > 1) {
265 $part = array_shift($parts);
266
267 if (isset($array[$part]) && is_array($array[$part])) {
268 $array = &$array[$part];
269 } else {
270 continue 2;
271 }
272 }
273
274 unset($array[array_shift($parts)]);
275 }
276 }
277
278 /**
279 * Get an item from an array using "dot" notation.
280 *
281 * @param \ArrayAccess|array $array
282 * @param string|int|null $key
283 * @param mixed $default
284 * @return mixed
285 */
286 public static function get($array, $key, $default = null)
287 {
288 if (! static::accessible($array)) {
289 return value($default);
290 }
291
292 if (is_null($key)) {
293 return $array;
294 }
295
296 if (static::exists($array, $key)) {
297 return $array[$key];
298 }
299
300 if (strpos($key, '.') === false) {
301 return $array[$key] ?? value($default);
302 }
303
304 foreach (explode('.', $key) as $segment) {
305 if (static::accessible($array) && static::exists($array, $segment)) {
306 $array = $array[$segment];
307 } else {
308 return value($default);
309 }
310 }
311
312 return $array;
313 }
314
315 /**
316 * Check if an item or items exist in an array using "dot" notation.
317 *
318 * @param \ArrayAccess|array $array
319 * @param string|array $keys
320 * @return bool
321 */
322 public static function has($array, $keys)
323 {
324 $keys = (array) $keys;
325
326 if (! $array || $keys === []) {
327 return false;
328 }
329
330 foreach ($keys as $key) {
331 $subKeyArray = $array;
332
333 if (static::exists($array, $key)) {
334 continue;
335 }
336
337 foreach (explode('.', $key) as $segment) {
338 if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) {
339 $subKeyArray = $subKeyArray[$segment];
340 } else {
341 return false;
342 }
343 }
344 }
345
346 return true;
347 }
348
349 /**
350 * Determine if any of the keys exist in an array using "dot" notation.
351 *
352 * @param \ArrayAccess|array $array
353 * @param string|array $keys
354 * @return bool
355 */
356 public static function hasAny($array, $keys)
357 {
358 if (is_null($keys)) {
359 return false;
360 }
361
362 $keys = (array) $keys;
363
364 if (! $array) {
365 return false;
366 }
367
368 if ($keys === []) {
369 return false;
370 }
371
372 foreach ($keys as $key) {
373 if (static::has($array, $key)) {
374 return true;
375 }
376 }
377
378 return false;
379 }
380
381 /**
382 * Determines if an array is associative.
383 *
384 * An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
385 *
386 * @param array $array
387 * @return bool
388 */
389 public static function isAssoc(array $array)
390 {
391 $keys = array_keys($array);
392
393 return array_keys($keys) !== $keys;
394 }
395
396 /**
397 * Get a subset of the items from the given array.
398 *
399 * @param array $array
400 * @param array|string $keys
401 * @return array
402 */
403 public static function only($array, $keys)
404 {
405 return array_intersect_key($array, array_flip((array) $keys));
406 }
407
408 /**
409 * Pluck an array of values from an array.
410 *
411 * @param iterable $array
412 * @param string|array|int|null $value
413 * @param string|array|null $key
414 * @return array
415 */
416 public static function pluck($array, $value, $key = null)
417 {
418 $results = [];
419
420 [$value, $key] = static::explodePluckParameters($value, $key);
421
422 foreach ($array as $item) {
423 $itemValue = data_get($item, $value);
424
425 // If the key is "null", we will just append the value to the array and keep
426 // looping. Otherwise we will key the array using the value of the key we
427 // received from the developer. Then we'll return the final array form.
428 if (is_null($key)) {
429 $results[] = $itemValue;
430 } else {
431 $itemKey = data_get($item, $key);
432
433 if (is_object($itemKey) && method_exists($itemKey, '__toString')) {
434 $itemKey = (string) $itemKey;
435 }
436
437 $results[$itemKey] = $itemValue;
438 }
439 }
440
441 return $results;
442 }
443
444 /**
445 * Explode the "value" and "key" arguments passed to "pluck".
446 *
447 * @param string|array $value
448 * @param string|array|null $key
449 * @return array
450 */
451 protected static function explodePluckParameters($value, $key)
452 {
453 $value = is_string($value) ? explode('.', $value) : $value;
454
455 $key = is_null($key) || is_array($key) ? $key : explode('.', $key);
456
457 return [$value, $key];
458 }
459
460 /**
461 * Push an item onto the beginning of an array.
462 *
463 * @param array $array
464 * @param mixed $value
465 * @param mixed $key
466 * @return array
467 */
468 public static function prepend($array, $value, $key = null)
469 {
470 if (func_num_args() == 2) {
471 array_unshift($array, $value);
472 } else {
473 $array = [$key => $value] + $array;
474 }
475
476 return $array;
477 }
478
479 /**
480 * Get a value from the array, and remove it.
481 *
482 * @param array $array
483 * @param string $key
484 * @param mixed $default
485 * @return mixed
486 */
487 public static function pull(&$array, $key, $default = null)
488 {
489 $value = static::get($array, $key, $default);
490
491 static::forget($array, $key);
492
493 return $value;
494 }
495
496 /**
497 * Get one or a specified number of random values from an array.
498 *
499 * @param array $array
500 * @param int|null $number
501 * @param bool|false $preserveKeys
502 * @return mixed
503 *
504 * @throws \InvalidArgumentException
505 */
506 public static function random($array, $number = null, $preserveKeys = false)
507 {
508 $requested = is_null($number) ? 1 : $number;
509
510 $count = count($array);
511
512 if ($requested > $count) {
513 throw new InvalidArgumentException(
514 "You requested {$requested} items, but there are only {$count} items available."
515 );
516 }
517
518 if (is_null($number)) {
519 return $array[array_rand($array)];
520 }
521
522 if ((int) $number === 0) {
523 return [];
524 }
525
526 $keys = array_rand($array, $number);
527
528 $results = [];
529
530 if ($preserveKeys) {
531 foreach ((array) $keys as $key) {
532 $results[$key] = $array[$key];
533 }
534 } else {
535 foreach ((array) $keys as $key) {
536 $results[] = $array[$key];
537 }
538 }
539
540 return $results;
541 }
542
543 /**
544 * Set an array item to a given value using "dot" notation.
545 *
546 * If no key is given to the method, the entire array will be replaced.
547 *
548 * @param array $array
549 * @param string|null $key
550 * @param mixed $value
551 * @return array
552 */
553 public static function set(&$array, $key, $value)
554 {
555 if (is_null($key)) {
556 return $array = $value;
557 }
558
559 $keys = explode('.', $key);
560
561 foreach ($keys as $i => $key) {
562 if (count($keys) === 1) {
563 break;
564 }
565
566 unset($keys[$i]);
567
568 // If the key doesn't exist at this depth, we will just create an empty array
569 // to hold the next value, allowing us to create the arrays to hold final
570 // values at the correct depth. Then we'll keep digging into the array.
571 if (! isset($array[$key]) || ! is_array($array[$key])) {
572 $array[$key] = [];
573 }
574
575 $array = &$array[$key];
576 }
577
578 $array[array_shift($keys)] = $value;
579
580 return $array;
581 }
582
583 /**
584 * Shuffle the given array and return the result.
585 *
586 * @param array $array
587 * @param int|null $seed
588 * @return array
589 */
590 public static function shuffle($array, $seed = null)
591 {
592 if (is_null($seed)) {
593 shuffle($array);
594 } else {
595 mt_srand($seed);
596 shuffle($array);
597 mt_srand();
598 }
599
600 return $array;
601 }
602
603 /**
604 * Sort the array using the given callback or "dot" notation.
605 *
606 * @param array $array
607 * @param callable|array|string|null $callback
608 * @return array
609 */
610 public static function sort($array, $callback = null)
611 {
612 return Collection::make($array)->sortBy($callback)->all();
613 }
614
615 /**
616 * Recursively sort an array by keys and values.
617 *
618 * @param array $array
619 * @param int $options
620 * @param bool $descending
621 * @return array
622 */
623 public static function sortRecursive($array, $options = SORT_REGULAR, $descending = false)
624 {
625 foreach ($array as &$value) {
626 if (is_array($value)) {
627 $value = static::sortRecursive($value, $options, $descending);
628 }
629 }
630
631 if (static::isAssoc($array)) {
632 $descending
633 ? krsort($array, $options)
634 : ksort($array, $options);
635 } else {
636 $descending
637 ? rsort($array, $options)
638 : sort($array, $options);
639 }
640
641 return $array;
642 }
643
644 /**
645 * Convert the array into a query string.
646 *
647 * @param array $array
648 * @return string
649 */
650 public static function query($array)
651 {
652 return http_build_query($array, '', '&', PHP_QUERY_RFC3986);
653 }
654
655 /**
656 * Filter the array using the given callback.
657 *
658 * @param array $array
659 * @param callable $callback
660 * @return array
661 */
662 public static function where($array, callable $callback)
663 {
664 return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
665 }
666
667 /**
668 * If the given value is not an array and not null, wrap it in one.
669 *
670 * @param mixed $value
671 * @return array
672 */
673 public static function wrap($value)
674 {
675 if (is_null($value)) {
676 return [];
677 }
678
679 return is_array($value) ? $value : [$value];
680 }
681}