1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * Platine Stdlib |
||||
5 | * |
||||
6 | * Platine Stdlib is a the collection of frequently used php features |
||||
7 | * |
||||
8 | * This content is released under the MIT License (MIT) |
||||
9 | * |
||||
10 | * Copyright (c) 2020 Platine Stdlib |
||||
11 | * |
||||
12 | * Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
13 | * of this software and associated documentation files (the "Software"), to deal |
||||
14 | * in the Software without restriction, including without limitation the rights |
||||
15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
16 | * copies of the Software, and to permit persons to whom the Software is |
||||
17 | * furnished to do so, subject to the following conditions: |
||||
18 | * |
||||
19 | * The above copyright notice and this permission notice shall be included in all |
||||
20 | * copies or substantial portions of the Software. |
||||
21 | * |
||||
22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
28 | * SOFTWARE. |
||||
29 | */ |
||||
30 | |||||
31 | /** |
||||
32 | * @file Arr.php |
||||
33 | * |
||||
34 | * The Array helper class |
||||
35 | * |
||||
36 | * @package Platine\Stdlib\Helper |
||||
37 | * @author Platine Developers Team |
||||
38 | * @copyright Copyright (c) 2020 |
||||
39 | * @license http://opensource.org/licenses/MIT MIT License |
||||
40 | * @link https://www.platine-php.com |
||||
41 | * @version 1.0.0 |
||||
42 | * @filesource |
||||
43 | */ |
||||
44 | |||||
45 | declare(strict_types=1); |
||||
46 | |||||
47 | namespace Platine\Stdlib\Helper; |
||||
48 | |||||
49 | use ArrayAccess; |
||||
50 | use Closure; |
||||
51 | use InvalidArgumentException; |
||||
52 | use Platine\Stdlib\Contract\Arrayable; |
||||
53 | use Stringable; |
||||
54 | use Traversable; |
||||
55 | |||||
56 | |||||
57 | /** |
||||
58 | * @class Arr |
||||
59 | * @package Platine\Stdlib\Helper |
||||
60 | */ |
||||
61 | class Arr |
||||
62 | { |
||||
63 | /** |
||||
64 | * Convert an array, object or string to array |
||||
65 | * @param array<mixed>|object|string $object |
||||
66 | * @param array<string, array<int|string, string>> $properties |
||||
67 | * @param bool $recursive |
||||
68 | * @return array<mixed> |
||||
69 | */ |
||||
70 | public static function toArray( |
||||
71 | array|object|string $object, |
||||
72 | array $properties = [], |
||||
73 | bool $recursive = true |
||||
74 | ): array { |
||||
75 | if (is_array($object)) { |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
76 | if ($recursive) { |
||||
77 | foreach ($object as $key => $value) { |
||||
78 | if (is_array($value) || is_object($value)) { |
||||
79 | $object[$key] = static::toArray($value, $properties, true); |
||||
80 | } |
||||
81 | } |
||||
82 | } |
||||
83 | |||||
84 | return $object; |
||||
85 | } |
||||
86 | |||||
87 | if (is_object($object)) { |
||||
88 | if (count($properties) > 0) { |
||||
89 | $className = get_class($object); |
||||
90 | if (count($properties[$className]) > 0) { |
||||
91 | $result = []; |
||||
92 | foreach ($properties[$className] as $key => $name) { |
||||
93 | if (is_int($key)) { |
||||
94 | $result[$name] = $object->{$name}; |
||||
95 | } else { |
||||
96 | $result[$key] = static::getValue($object, $name); |
||||
97 | } |
||||
98 | } |
||||
99 | |||||
100 | return $recursive |
||||
101 | ? static::toArray($result, $properties) |
||||
102 | : $result; |
||||
103 | } |
||||
104 | } |
||||
105 | |||||
106 | if ($object instanceof Arrayable) { |
||||
107 | $result = $object->toArray(); |
||||
108 | } else { |
||||
109 | $result = []; |
||||
110 | foreach ((array)$object as $key => $value) { |
||||
111 | $result[$key] = $value; |
||||
112 | } |
||||
113 | } |
||||
114 | |||||
115 | return $recursive |
||||
116 | ? static::toArray($result, $properties) |
||||
117 | : $result; |
||||
118 | } |
||||
119 | |||||
120 | return [$object]; |
||||
121 | } |
||||
122 | |||||
123 | /** |
||||
124 | * Merge the passed arrays |
||||
125 | * @param array<mixed> ...$args |
||||
126 | * @return array<mixed> |
||||
127 | */ |
||||
128 | public static function merge(array ...$args): array |
||||
129 | { |
||||
130 | $res = (array)array_shift($args); |
||||
131 | while (count($args) > 0) { |
||||
132 | $next = array_shift($args); |
||||
133 | foreach ($next as $key => $value) { |
||||
134 | if (is_int($key)) { |
||||
135 | if (isset($res[$key])) { |
||||
136 | $res[] = $value; |
||||
137 | } else { |
||||
138 | $res[$key] = $value; |
||||
139 | } |
||||
140 | } elseif (is_array($value) && isset($res[$key]) && is_array($res[$key])) { |
||||
141 | $res[$key] = self::merge($res[$key], $value); |
||||
142 | } else { |
||||
143 | $res[$key] = $value; |
||||
144 | } |
||||
145 | } |
||||
146 | } |
||||
147 | |||||
148 | return $res; |
||||
149 | } |
||||
150 | |||||
151 | /** |
||||
152 | * Return the value of an array element or object property |
||||
153 | * for the given key or property name. |
||||
154 | * |
||||
155 | * @param mixed $object |
||||
156 | * @param int|string|Closure|array<mixed> $key |
||||
157 | * @param mixed $default |
||||
158 | * |
||||
159 | * @return mixed If the key does not exist in the array or object, |
||||
160 | * the default value will be returned instead. |
||||
161 | */ |
||||
162 | public static function getValue( |
||||
163 | mixed $object, |
||||
164 | int|string|Closure|array $key, |
||||
165 | mixed $default = null |
||||
166 | ): mixed { |
||||
167 | if ($key instanceof Closure) { |
||||
0 ignored issues
–
show
|
|||||
168 | return $key($object, $default); |
||||
169 | } |
||||
170 | |||||
171 | if (is_array($key)) { |
||||
0 ignored issues
–
show
|
|||||
172 | $lastKey = array_pop($key); |
||||
173 | foreach ($key as $part) { |
||||
174 | $object = static::getValue($object, $part); |
||||
175 | } |
||||
176 | $key = $lastKey; |
||||
177 | } |
||||
178 | |||||
179 | if ( |
||||
180 | is_array($object) && (isset($object[$key]) |
||||
181 | || array_key_exists($key, $object)) |
||||
182 | ) { |
||||
183 | return $object[$key]; |
||||
184 | } |
||||
185 | |||||
186 | if (is_string($key)) { |
||||
187 | $pos = strpos($key, '.'); |
||||
188 | |||||
189 | if ($pos !== false) { |
||||
190 | $object = static::getValue( |
||||
191 | $object, |
||||
192 | substr($key, 0, $pos), |
||||
193 | $default |
||||
194 | ); |
||||
195 | $key = (string) substr($key, $pos + 1); |
||||
196 | } |
||||
197 | } |
||||
198 | |||||
199 | // Note: property_exists not detected property with magic |
||||
200 | // Method so add isset for this purpose |
||||
201 | if (is_object($object) && (property_exists($object, $key) || isset($object->{$key}))) { |
||||
202 | // this is will fail if the property does not exist, |
||||
203 | // or __get() is not implemented |
||||
204 | // it is not reliably possible to check whether a property |
||||
205 | // is accessable beforehand |
||||
206 | |||||
207 | return $object->{$key}; |
||||
208 | } |
||||
209 | |||||
210 | if (is_array($object)) { |
||||
211 | return (isset($object[$key]) || array_key_exists($key, $object)) |
||||
212 | ? $object[$key] |
||||
213 | : $default; |
||||
214 | } |
||||
215 | |||||
216 | return $default; |
||||
217 | } |
||||
218 | |||||
219 | /** |
||||
220 | * Remove an item from an array and returns the value. |
||||
221 | * If the key does not exist in the array, the default value will be returned |
||||
222 | * @param array<mixed> $array |
||||
223 | * @param string|int $key |
||||
224 | * @param mixed $default |
||||
225 | * |
||||
226 | * @return mixed|null |
||||
227 | */ |
||||
228 | public static function remove(array &$array, string|int $key, mixed $default = null): mixed |
||||
229 | { |
||||
230 | if (isset($array[$key]) || array_key_exists($key, $array)) { |
||||
231 | $value = $array[$key]; |
||||
232 | |||||
233 | unset($array[$key]); |
||||
234 | |||||
235 | return $value; |
||||
236 | } |
||||
237 | |||||
238 | return $default; |
||||
239 | } |
||||
240 | |||||
241 | /** |
||||
242 | * Return all of the given array except for a specified keys. |
||||
243 | * @param array<mixed> $array |
||||
244 | * @param array<int, int|string>|string|int $keys |
||||
245 | * @return array<mixed> |
||||
246 | */ |
||||
247 | public static function except(array $array, array|string|int $keys): array |
||||
248 | { |
||||
249 | static::forget($array, $keys); |
||||
250 | |||||
251 | return $array; |
||||
252 | } |
||||
253 | |||||
254 | /** |
||||
255 | * Remove one or many array items from a given array using "dot" notation. |
||||
256 | * @param array<mixed> $array |
||||
257 | * @param array<int, int|string>|string|int $keys |
||||
258 | * @return void |
||||
259 | */ |
||||
260 | public static function forget(array &$array, array|string|int $keys): void |
||||
261 | { |
||||
262 | $original = &$array; |
||||
263 | |||||
264 | if (!is_array($keys)) { |
||||
0 ignored issues
–
show
|
|||||
265 | $keys = [$keys]; |
||||
266 | } |
||||
267 | |||||
268 | if (count($keys) === 0) { |
||||
269 | return; |
||||
270 | } |
||||
271 | |||||
272 | foreach ($keys as $key) { |
||||
273 | if (static::exists($array, $key)) { |
||||
274 | unset($array[$key]); |
||||
275 | |||||
276 | continue; |
||||
277 | } |
||||
278 | |||||
279 | if (is_string($key)) { |
||||
280 | $parts = explode('.', $key); |
||||
281 | |||||
282 | $array = &$original; |
||||
283 | while (count($parts) > 1) { |
||||
284 | $part = array_shift($parts); |
||||
285 | if (isset($array[$part]) && is_array($array[$part])) { |
||||
286 | $array = &$array[$part]; |
||||
287 | } else { |
||||
288 | continue 2; |
||||
289 | } |
||||
290 | } |
||||
291 | |||||
292 | unset($array[array_shift($parts)]); |
||||
293 | } |
||||
294 | } |
||||
295 | } |
||||
296 | |||||
297 | /** |
||||
298 | * Get a value from the array, and remove it. |
||||
299 | * @param array<mixed> $array |
||||
300 | * @param string|int $key |
||||
301 | * @param mixed $default |
||||
302 | * @return mixed |
||||
303 | */ |
||||
304 | public static function pull(array &$array, string|int $key, mixed $default = null): mixed |
||||
305 | { |
||||
306 | $value = static::get($array, $key, $default); |
||||
307 | |||||
308 | static::forget($array, $key); |
||||
309 | |||||
310 | return $value; |
||||
311 | } |
||||
312 | |||||
313 | /** |
||||
314 | * Indexes and/or groups the array according to a specified key. |
||||
315 | * The input should be either multidimensional array or an array of objects. |
||||
316 | * |
||||
317 | * The $key can be either a key name of the sub-array, a property |
||||
318 | * name of object, or an anonymous function that must return the value |
||||
319 | * that will be used as a key. |
||||
320 | * |
||||
321 | * $groups is an array of keys, that will be used to group the input |
||||
322 | * array into one or more sub-arrays based on keys specified. |
||||
323 | * |
||||
324 | * If the `$key` is specified as `null` or a value of an element |
||||
325 | * corresponding to the key is `null` in addition to `$groups` not |
||||
326 | * specified then the element is discarded. |
||||
327 | * |
||||
328 | * @param array<mixed> $array |
||||
329 | * @param string|int|Closure|array<mixed>|null $key |
||||
330 | * @param string|int|string[]|int[]|Closure[]|null $groups |
||||
331 | * @return array<mixed> the indexed and/or grouped array |
||||
332 | */ |
||||
333 | public static function index( |
||||
334 | array $array, |
||||
335 | string|int|Closure|array|null $key = null, |
||||
336 | string|int|array|null $groups = [] |
||||
337 | ): array { |
||||
338 | $result = []; |
||||
339 | if (!is_array($groups)) { |
||||
0 ignored issues
–
show
|
|||||
340 | $groups = (array) $groups; |
||||
341 | } |
||||
342 | |||||
343 | foreach ($array as $element) { |
||||
344 | $lastArray = &$result; |
||||
345 | foreach ($groups as $group) { |
||||
346 | /** @var int|string $value */ |
||||
347 | $value = static::getValue($element, $group); |
||||
348 | if (count($lastArray) > 0 && !array_key_exists($value, $lastArray)) { |
||||
349 | $lastArray[$value] = []; |
||||
350 | } |
||||
351 | $lastArray = &$lastArray[$value]; |
||||
352 | } |
||||
353 | |||||
354 | if ($key === null) { |
||||
355 | if (count($groups) > 0) { |
||||
356 | $lastArray[] = $element; |
||||
357 | } |
||||
358 | } else { |
||||
359 | $value = static::getValue($element, $key); |
||||
360 | if ($value !== null) { |
||||
361 | if (is_float($value)) { |
||||
362 | $value = (string) $value; |
||||
363 | } |
||||
364 | $lastArray[$value] = $element; |
||||
365 | } |
||||
366 | } |
||||
367 | unset($lastArray); |
||||
368 | } |
||||
369 | |||||
370 | return $result; |
||||
371 | } |
||||
372 | |||||
373 | /** |
||||
374 | * Returns the values of a specified column in an array. |
||||
375 | * The input array should be multidimensional or an array of objects. |
||||
376 | * @param array<mixed> $array |
||||
377 | * @param int|string|Closure $name |
||||
378 | * @param bool $keepKeys |
||||
379 | * @return array<mixed> |
||||
380 | */ |
||||
381 | public static function getColumn( |
||||
382 | array $array, |
||||
383 | int|string|Closure $name, |
||||
384 | bool $keepKeys = true |
||||
385 | ): array { |
||||
386 | $result = []; |
||||
387 | if ($keepKeys) { |
||||
388 | foreach ($array as $key => $element) { |
||||
389 | $result[$key] = static::getValue($element, $name); |
||||
390 | } |
||||
391 | } else { |
||||
392 | foreach ($array as $element) { |
||||
393 | $result[] = static::getValue($element, $name); |
||||
394 | } |
||||
395 | } |
||||
396 | |||||
397 | return $result; |
||||
398 | } |
||||
399 | |||||
400 | /** |
||||
401 | * Builds a map (key-value pairs) from a multidimensional array or |
||||
402 | * an array of objects. |
||||
403 | * The `$from` and `$to` parameters specify the key names or property |
||||
404 | * names to set up the map. |
||||
405 | * Optionally, one can further group the map according to a |
||||
406 | * grouping field `$group`. |
||||
407 | * |
||||
408 | * @param array<mixed> $array |
||||
409 | * @param string|Closure $from |
||||
410 | * @param string|Closure $to |
||||
411 | * @param string|array<mixed>|Closure|null $group |
||||
412 | * @return array<mixed> |
||||
413 | */ |
||||
414 | public static function map( |
||||
415 | array $array, |
||||
416 | string|Closure $from, |
||||
417 | string|Closure $to, |
||||
418 | string|array|Closure|null $group = null |
||||
419 | ): array { |
||||
420 | $result = []; |
||||
421 | foreach ($array as $element) { |
||||
422 | $key = static::getValue($element, $from); |
||||
423 | $value = static::getValue($element, $to); |
||||
424 | if ($group !== null) { |
||||
425 | $result[static::getValue($element, $group)][$key] = $value; |
||||
426 | } else { |
||||
427 | $result[$key] = $value; |
||||
428 | } |
||||
429 | } |
||||
430 | |||||
431 | return $result; |
||||
432 | } |
||||
433 | |||||
434 | /** |
||||
435 | * Checks if the given array contains the specified key. |
||||
436 | * This method enhances the `array_key_exists()` function by supporting |
||||
437 | * case-insensitive key comparison. |
||||
438 | * |
||||
439 | * @param string $key |
||||
440 | * @param array<mixed> $array |
||||
441 | * @param bool $caseSensative |
||||
442 | * @return bool |
||||
443 | */ |
||||
444 | public static function keyExists( |
||||
445 | string $key, |
||||
446 | array $array, |
||||
447 | bool $caseSensative = true |
||||
448 | ): bool { |
||||
449 | if ($caseSensative) { |
||||
450 | return array_key_exists($key, $array); |
||||
451 | } |
||||
452 | |||||
453 | foreach (array_keys($array) as $k) { |
||||
454 | if (strcasecmp($key, $k) === 0) { |
||||
455 | return true; |
||||
456 | } |
||||
457 | } |
||||
458 | |||||
459 | return false; |
||||
460 | } |
||||
461 | |||||
462 | /** |
||||
463 | * Sorts an array of objects or arrays (with the same structure) by one |
||||
464 | * or several keys. |
||||
465 | * @param array<mixed> $array |
||||
466 | * @param string|Closure|array<string> $key the key(s) to be sorted by. |
||||
467 | * This refers to a key name of the sub-array elements, a property name |
||||
468 | * of the objects, or an anonymous function returning the values |
||||
469 | * for comparison purpose. The anonymous function signature |
||||
470 | * should be: `function($item)`. |
||||
471 | * @param int|array<int> $direction |
||||
472 | * @param int|array<int> $sortFlag |
||||
473 | * @return void |
||||
474 | */ |
||||
475 | public static function multisort( |
||||
476 | array &$array, |
||||
477 | string|Closure|array $key, |
||||
478 | int|array $direction = SORT_ASC, |
||||
479 | int|array $sortFlag = SORT_REGULAR |
||||
480 | ): void { |
||||
481 | $keys = is_array($key) ? $key : [$key]; |
||||
0 ignored issues
–
show
|
|||||
482 | |||||
483 | if (empty($keys) || empty($array)) { |
||||
484 | return; |
||||
485 | } |
||||
486 | |||||
487 | $count = count($keys); |
||||
488 | if (is_scalar($direction)) { |
||||
489 | $direction = array_fill(0, $count, $direction); |
||||
490 | } elseif (count($direction) !== $count) { |
||||
491 | throw new InvalidArgumentException( |
||||
492 | 'The length of the sort direction must be the same ' |
||||
493 | . 'as that of sort keys.' |
||||
494 | ); |
||||
495 | } |
||||
496 | if (is_scalar($sortFlag)) { |
||||
497 | $sortFlag = array_fill(0, $count, $sortFlag); |
||||
498 | } elseif (count($sortFlag) !== $count) { |
||||
499 | throw new InvalidArgumentException( |
||||
500 | 'The length of the sort flag must be the same ' |
||||
501 | . 'as that of sort keys.' |
||||
502 | ); |
||||
503 | } |
||||
504 | |||||
505 | /** @var array<int, mixed> $args */ |
||||
506 | $args = []; |
||||
507 | foreach ($keys as $i => $k) { |
||||
508 | $flag = $sortFlag[$i]; |
||||
509 | $args[] = static::getColumn($array, $k); |
||||
510 | $args[] = $direction[$i]; |
||||
511 | $args[] = $flag; |
||||
512 | } |
||||
513 | |||||
514 | // This fix is used for cases when main sorting specified by |
||||
515 | // columns has equal values |
||||
516 | // Without it it will lead to Fatal Error: Nesting level |
||||
517 | // too deep - recursive dependency? |
||||
518 | $args[] = range(1, count($array)); |
||||
519 | $args[] = SORT_ASC; |
||||
520 | $args[] = SORT_NUMERIC; |
||||
521 | $args[] = &$array; |
||||
522 | |||||
523 | array_multisort(...$args); |
||||
524 | } |
||||
525 | |||||
526 | /** |
||||
527 | * Check whether the given array is an associative array. |
||||
528 | * |
||||
529 | * An array is associative if all its keys are strings. |
||||
530 | * If `$allStrings` is false, then an array will be treated as associative |
||||
531 | * if at least one of its keys is a string. |
||||
532 | * |
||||
533 | * Note that an empty array will NOT be considered associative. |
||||
534 | * |
||||
535 | * @param array<mixed> $array |
||||
536 | * @param bool $allStrings |
||||
537 | * @return bool |
||||
538 | */ |
||||
539 | public static function isAssoc(array $array, bool $allStrings = true): bool |
||||
540 | { |
||||
541 | if (empty($array)) { |
||||
542 | return false; |
||||
543 | } |
||||
544 | |||||
545 | if ($allStrings) { |
||||
546 | foreach ($array as $key => $value) { |
||||
547 | if (!is_string($key)) { |
||||
548 | return false; |
||||
549 | } |
||||
550 | } |
||||
551 | |||||
552 | return true; |
||||
553 | } else { |
||||
554 | foreach ($array as $key => $value) { |
||||
555 | if (is_string($key)) { |
||||
556 | return true; |
||||
557 | } |
||||
558 | } |
||||
559 | |||||
560 | return false; |
||||
561 | } |
||||
562 | } |
||||
563 | |||||
564 | /** |
||||
565 | * Whether the given array is an indexed array. |
||||
566 | * |
||||
567 | * An array is indexed if all its keys are integers. |
||||
568 | * If `$consecutive` is true, then the array keys must be a consecutive |
||||
569 | * sequence starting from 0. |
||||
570 | * |
||||
571 | * Note that an empty array will be considered indexed. |
||||
572 | * |
||||
573 | * @param array<mixed> $array |
||||
574 | * @param bool $consecutive |
||||
575 | * @return bool |
||||
576 | */ |
||||
577 | public static function isIndexed(array $array, bool $consecutive = false): bool |
||||
578 | { |
||||
579 | if (empty($array)) { |
||||
580 | return true; |
||||
581 | } |
||||
582 | |||||
583 | if ($consecutive) { |
||||
584 | return array_keys($array) === range(0, count($array) - 1); |
||||
585 | } else { |
||||
586 | foreach ($array as $key => $value) { |
||||
587 | if (!is_int($key)) { |
||||
588 | return false; |
||||
589 | } |
||||
590 | } |
||||
591 | |||||
592 | return true; |
||||
593 | } |
||||
594 | } |
||||
595 | |||||
596 | /** |
||||
597 | * Check whether an array or Traversable contains an element. |
||||
598 | * |
||||
599 | * This method does the same as the PHP function in_array() |
||||
600 | * but additionally works for objects that implement the |
||||
601 | * Traversable interface. |
||||
602 | * |
||||
603 | * @param mixed $needle |
||||
604 | * @param array<mixed>|Traversable<int|string, mixed> $array |
||||
605 | * @param bool $strict |
||||
606 | * @return bool |
||||
607 | */ |
||||
608 | public static function isIn(mixed $needle, array|Traversable $array, bool $strict = false): bool |
||||
609 | { |
||||
610 | if ($array instanceof Traversable) { |
||||
0 ignored issues
–
show
|
|||||
611 | $array = iterator_to_array($array); |
||||
612 | } |
||||
613 | |||||
614 | foreach ($array as $value) { |
||||
615 | if ($needle == $value && (!$strict || $needle === $value)) { |
||||
616 | return true; |
||||
617 | } |
||||
618 | } |
||||
619 | |||||
620 | |||||
621 | return in_array($needle, $array, $strict); |
||||
622 | } |
||||
623 | |||||
624 | /** |
||||
625 | * Checks whether a variable is an array or Traversable. |
||||
626 | * @param mixed $var |
||||
627 | * @return bool |
||||
628 | */ |
||||
629 | public static function isTraversable(mixed $var): bool |
||||
630 | { |
||||
631 | return is_array($var) || $var instanceof Traversable; |
||||
632 | } |
||||
633 | |||||
634 | /** |
||||
635 | * Checks whether an array or Traversable is a subset of another array |
||||
636 | * or Traversable. |
||||
637 | * |
||||
638 | * This method will return `true`, if all elements of `$needles` |
||||
639 | * are contained in `$array`. If at least one element is missing, |
||||
640 | * `false` will be returned. |
||||
641 | * |
||||
642 | * @param array<mixed>|Traversable<int|string, mixed> $needles |
||||
643 | * @param array<mixed>|Traversable<int|string, mixed> $array |
||||
644 | * @param bool $strict |
||||
645 | * @return bool |
||||
646 | */ |
||||
647 | public static function isSubset( |
||||
648 | array|Traversable $needles, |
||||
649 | array|Traversable $array, |
||||
650 | bool $strict = false |
||||
651 | ): bool { |
||||
652 | foreach ($needles as $needle) { |
||||
653 | if (!static::isIn($needle, $array, $strict)) { |
||||
654 | return false; |
||||
655 | } |
||||
656 | } |
||||
657 | |||||
658 | return true; |
||||
659 | } |
||||
660 | |||||
661 | /** |
||||
662 | * Filters array according to rules specified. |
||||
663 | * @param array<mixed> $array |
||||
664 | * @param array<string> $filters Rules that define array keys which should |
||||
665 | * be left or removed from results. |
||||
666 | * Each rule is: |
||||
667 | * - `var` - `$array['var']` will be left in result. |
||||
668 | * - `var.key` = only `$array['var']['key'] will be left in result. |
||||
669 | * - `!var.key` = `$array['var']['key'] will be removed from result. |
||||
670 | * @return array<mixed> |
||||
671 | */ |
||||
672 | public static function filter(array $array, array $filters): array |
||||
673 | { |
||||
674 | $result = []; |
||||
675 | $tobeRemoved = []; |
||||
676 | |||||
677 | foreach ($filters as $filter) { |
||||
678 | $keys = explode('.', $filter); |
||||
679 | $globalKey = $keys[0]; |
||||
680 | $localkey = $keys[1] ?? null; |
||||
681 | if ($globalKey[0] === '!') { |
||||
682 | $tobeRemoved[] = [ |
||||
683 | substr($globalKey, 1), |
||||
684 | $localkey |
||||
685 | ]; |
||||
686 | |||||
687 | continue; |
||||
688 | } |
||||
689 | |||||
690 | if (empty($array[$globalKey])) { |
||||
691 | continue; |
||||
692 | } |
||||
693 | |||||
694 | if ($localkey === null) { |
||||
695 | $result[$globalKey] = $array[$globalKey]; |
||||
696 | continue; |
||||
697 | } |
||||
698 | |||||
699 | if (!isset($array[$globalKey][$localkey])) { |
||||
700 | continue; |
||||
701 | } |
||||
702 | |||||
703 | if (array_key_exists($globalKey, $result)) { |
||||
704 | $result[$globalKey] = []; |
||||
705 | } |
||||
706 | |||||
707 | $result[$globalKey][$localkey] = $array[$globalKey][$localkey]; |
||||
708 | } |
||||
709 | |||||
710 | foreach ($tobeRemoved as $value) { |
||||
711 | [$globalKey, $localkey] = $value; |
||||
712 | if (array_key_exists($globalKey, $result)) { |
||||
713 | unset($result[$globalKey][$localkey]); |
||||
714 | } |
||||
715 | } |
||||
716 | |||||
717 | return $result; |
||||
718 | } |
||||
719 | |||||
720 | /** |
||||
721 | * Checks whether a variable is an array accessible. |
||||
722 | * @param mixed $var |
||||
723 | * @return bool |
||||
724 | */ |
||||
725 | public static function isAccessible(mixed $var): bool |
||||
726 | { |
||||
727 | return is_array($var) || $var instanceof ArrayAccess; |
||||
728 | } |
||||
729 | |||||
730 | /** |
||||
731 | * Checks whether a variable is an array or instance of Arrayable. |
||||
732 | * @param mixed $var |
||||
733 | * @return bool |
||||
734 | */ |
||||
735 | public static function isArrayable(mixed $var): bool |
||||
736 | { |
||||
737 | return is_array($var) || $var instanceof Arrayable; |
||||
738 | } |
||||
739 | |||||
740 | /** |
||||
741 | * If the given value is not an array and not null, wrap it in one. |
||||
742 | * @param mixed $var |
||||
743 | * @return array<mixed> |
||||
744 | */ |
||||
745 | public static function wrap(mixed $var): array |
||||
746 | { |
||||
747 | if ($var === null) { |
||||
748 | return []; |
||||
749 | } |
||||
750 | return is_array($var) ? $var : [$var]; |
||||
751 | } |
||||
752 | |||||
753 | /** |
||||
754 | * Check whether the given key exists in the provided array. |
||||
755 | * |
||||
756 | * @param array<mixed>|ArrayAccess<string|int, mixed> $array |
||||
757 | * @param string|int $key |
||||
758 | * @return bool |
||||
759 | */ |
||||
760 | public static function exists(array|ArrayAccess $array, string|int $key): bool |
||||
761 | { |
||||
762 | if (is_array($array)) { |
||||
0 ignored issues
–
show
|
|||||
763 | return array_key_exists($key, $array); |
||||
764 | } |
||||
765 | |||||
766 | return $array->offsetExists($key); |
||||
767 | } |
||||
768 | |||||
769 | /** |
||||
770 | * Get an item from an array using "dot" notation. |
||||
771 | * |
||||
772 | * @param array<mixed>|ArrayAccess<string|int, mixed> $array |
||||
773 | * @param string|int|null $key |
||||
774 | * @param mixed $default |
||||
775 | * @return mixed |
||||
776 | */ |
||||
777 | public static function get( |
||||
778 | array|ArrayAccess $array, |
||||
779 | string|int|null $key = null, |
||||
780 | mixed $default = null |
||||
781 | ): mixed { |
||||
782 | if ($key === null) { |
||||
783 | return $array; |
||||
784 | } |
||||
785 | |||||
786 | if (isset($array[$key])) { |
||||
787 | return $array[$key]; |
||||
788 | } |
||||
789 | |||||
790 | // Fix: If is int, stop continue find. |
||||
791 | if (!is_string($key)) { |
||||
792 | return $default; |
||||
793 | } |
||||
794 | |||||
795 | foreach (explode('.', $key) as $segment) { |
||||
796 | if (static::isAccessible($array) && static::exists($array, $segment)) { |
||||
797 | $array = $array[$segment]; |
||||
798 | } else { |
||||
799 | return $default; |
||||
800 | } |
||||
801 | } |
||||
802 | |||||
803 | return $array; |
||||
804 | } |
||||
805 | |||||
806 | /** |
||||
807 | * Check if an item exists in an array using "dot" notation. |
||||
808 | * |
||||
809 | * @param array<mixed>|ArrayAccess<string|int, mixed> $array |
||||
810 | * @param string|int $key |
||||
811 | * @return bool |
||||
812 | */ |
||||
813 | public static function has(array|ArrayAccess $array, string|int $key): bool |
||||
814 | { |
||||
815 | if (empty($array)) { |
||||
816 | return false; |
||||
817 | } |
||||
818 | |||||
819 | if ( |
||||
820 | (is_array($array) && array_key_exists($key, $array)) |
||||
821 | || ($array instanceof ArrayAccess && $array->offsetExists($key)) |
||||
822 | ) { |
||||
823 | return true; |
||||
824 | } |
||||
825 | |||||
826 | // Fix: If is int, stop continue find. |
||||
827 | if (!is_string($key)) { |
||||
828 | return false; |
||||
829 | } |
||||
830 | |||||
831 | foreach (explode('.', $key) as $segment) { |
||||
832 | if ( |
||||
833 | ((is_array($array) && array_key_exists($segment, $array)) |
||||
834 | || ($array instanceof ArrayAccess |
||||
835 | && $array->offsetExists($segment))) |
||||
836 | ) { |
||||
837 | $array = $array[$segment]; |
||||
838 | } else { |
||||
839 | return false; |
||||
840 | } |
||||
841 | } |
||||
842 | |||||
843 | return true; |
||||
844 | } |
||||
845 | |||||
846 | /** |
||||
847 | * |
||||
848 | * @param array<mixed> $array |
||||
849 | * @param string|null $key |
||||
850 | * @param mixed $value |
||||
851 | * @return void |
||||
852 | */ |
||||
853 | public static function set(array &$array, ?string $key, mixed $value): void |
||||
854 | { |
||||
855 | if ($key === null) { |
||||
856 | return; |
||||
857 | } |
||||
858 | |||||
859 | $keys = explode('.', $key); |
||||
860 | |||||
861 | while (count($keys) > 1) { |
||||
862 | $key = array_shift($keys); |
||||
863 | |||||
864 | // If the key doesn't exist at this depth, we will just create |
||||
865 | // an empty array hold the next value, allowing us to create |
||||
866 | // the arrays to hold final values at the correct depth. |
||||
867 | // Then we'll keep digging into the array. |
||||
868 | if (!isset($array[$key]) || !is_array($array[$key])) { |
||||
869 | $array[$key] = []; |
||||
870 | } |
||||
871 | |||||
872 | $array = &$array[$key]; |
||||
873 | } |
||||
874 | |||||
875 | $array[array_shift($keys)] = $value; |
||||
876 | } |
||||
877 | |||||
878 | /** |
||||
879 | * Insert one array to another array |
||||
880 | * |
||||
881 | * @param array<mixed> $array |
||||
882 | * @param int $index |
||||
883 | * @param array<mixed> ...$inserts |
||||
884 | * @return void |
||||
885 | */ |
||||
886 | public static function insert( |
||||
887 | array &$array, |
||||
888 | int $index, |
||||
889 | array ...$inserts |
||||
890 | ): void { |
||||
891 | $first = array_splice($array, 0, $index); |
||||
892 | $array = array_merge($first, $inserts, $array); |
||||
893 | } |
||||
894 | |||||
895 | /** |
||||
896 | * Flatten a multi-dimensional array into a single level. |
||||
897 | * @param array<mixed> $array |
||||
898 | * @param int $depth |
||||
899 | * @return array<mixed> |
||||
900 | */ |
||||
901 | public static function flatten(array $array, int $depth = PHP_INT_MAX): array |
||||
902 | { |
||||
903 | $result = []; |
||||
904 | foreach ($array as $item) { |
||||
905 | if (!is_array($item)) { |
||||
906 | $result[] = $item; |
||||
907 | } elseif ($depth === 1) { |
||||
908 | $result = array_merge($result, array_values($item)); |
||||
909 | } else { |
||||
910 | $result = array_merge( |
||||
911 | $result, |
||||
912 | static::flatten($item, $depth - 1) |
||||
913 | ); |
||||
914 | } |
||||
915 | } |
||||
916 | |||||
917 | return $result; |
||||
918 | } |
||||
919 | |||||
920 | /** |
||||
921 | * find similar text from an array |
||||
922 | * |
||||
923 | * @param string $need |
||||
924 | * @param array<int, string>|Traversable<int|string, mixed> $array |
||||
925 | * @param int $percentage |
||||
926 | * @return array<int, string> |
||||
927 | */ |
||||
928 | public static function similar( |
||||
929 | string $need, |
||||
930 | array|Traversable $array, |
||||
931 | int $percentage = 45 |
||||
932 | ): array { |
||||
933 | if (empty($need)) { |
||||
934 | return []; |
||||
935 | } |
||||
936 | |||||
937 | $similar = []; |
||||
938 | $percent = 0; |
||||
939 | foreach ($array as $name) { |
||||
940 | similar_text($need, $name, $percent); |
||||
941 | if ($percentage <= (int) $percent) { |
||||
942 | $similar[] = $name; |
||||
943 | } |
||||
944 | } |
||||
945 | |||||
946 | return $similar; |
||||
947 | } |
||||
948 | |||||
949 | /** |
||||
950 | * Return the array key max width |
||||
951 | * @param array<int|string, mixed> $array |
||||
952 | * @param bool $expectInt |
||||
953 | * @return int |
||||
954 | */ |
||||
955 | public static function getKeyMaxWidth(array $array, bool $expectInt = false): int |
||||
956 | { |
||||
957 | $max = 0; |
||||
958 | foreach ($array as $key => $value) { |
||||
959 | if (!$expectInt || !is_numeric($key)) { |
||||
960 | $width = mb_strlen((string)$key, 'UTF-8'); |
||||
961 | if ($width > $max) { |
||||
962 | $max = $width; |
||||
963 | } |
||||
964 | } |
||||
965 | } |
||||
966 | |||||
967 | return $max; |
||||
968 | } |
||||
969 | |||||
970 | /** |
||||
971 | * Return the first element in an array passing a given truth test. |
||||
972 | * @param array<mixed> $array |
||||
973 | * @param callable $callable |
||||
974 | * @param mixed $default |
||||
975 | * |
||||
976 | * @return mixed |
||||
977 | */ |
||||
978 | public static function first( |
||||
979 | array $array, |
||||
980 | callable $callable = null, |
||||
981 | mixed $default = null |
||||
982 | ): mixed { |
||||
983 | if ($callable === null) { |
||||
984 | if (count($array) === 0) { |
||||
985 | return $default; |
||||
986 | } |
||||
987 | |||||
988 | foreach ($array as $value) { |
||||
989 | return $value; |
||||
990 | } |
||||
991 | } |
||||
992 | |||||
993 | foreach ($array as $key => $value) { |
||||
994 | if ($callable($value, $key)) { |
||||
995 | return $value; |
||||
996 | } |
||||
997 | } |
||||
998 | |||||
999 | return $default; |
||||
1000 | } |
||||
1001 | |||||
1002 | /** |
||||
1003 | * Return the last element in an array passing a given truth test. |
||||
1004 | * @param array<mixed> $array |
||||
1005 | * @param callable $callable |
||||
1006 | * @param mixed $default |
||||
1007 | * |
||||
1008 | * @return mixed |
||||
1009 | */ |
||||
1010 | public static function last( |
||||
1011 | array $array, |
||||
1012 | callable $callable = null, |
||||
1013 | mixed $default = null |
||||
1014 | ): mixed { |
||||
1015 | if ($callable === null) { |
||||
1016 | if (count($array) === 0) { |
||||
1017 | return $default; |
||||
1018 | } |
||||
1019 | |||||
1020 | return end($array); |
||||
1021 | } |
||||
1022 | |||||
1023 | return static::first(array_reverse($array, true), $callable, $default); |
||||
1024 | } |
||||
1025 | |||||
1026 | /** |
||||
1027 | * Filter the array using the given callback. |
||||
1028 | * @param array<mixed> $array |
||||
1029 | * @param callable $callable |
||||
1030 | * @return array<mixed> |
||||
1031 | */ |
||||
1032 | public static function where(array $array, callable $callable): array |
||||
1033 | { |
||||
1034 | return array_filter($array, $callable, ARRAY_FILTER_USE_BOTH); |
||||
1035 | } |
||||
1036 | |||||
1037 | /** |
||||
1038 | * Convert the array into a query string. |
||||
1039 | * @param array<mixed> $array |
||||
1040 | * @return string |
||||
1041 | */ |
||||
1042 | public static function query(array $array): string |
||||
1043 | { |
||||
1044 | return http_build_query($array, '', '&', PHP_QUERY_RFC3986); |
||||
1045 | } |
||||
1046 | |||||
1047 | /** |
||||
1048 | * Get a subset of the items from the given array. |
||||
1049 | * @param array<mixed> $array |
||||
1050 | * @param array<int, int|string> $keys |
||||
1051 | * @return array<mixed> |
||||
1052 | */ |
||||
1053 | public static function only(array $array, array $keys): array |
||||
1054 | { |
||||
1055 | return array_intersect_key($array, array_flip($keys)); |
||||
1056 | } |
||||
1057 | |||||
1058 | /** |
||||
1059 | * Pluck an array of values from an arrays. |
||||
1060 | * @param array<int, array<mixed>> $array |
||||
1061 | * @param string|int $value |
||||
1062 | * @param string|int|null $key |
||||
1063 | * @return array<mixed> |
||||
1064 | */ |
||||
1065 | public static function pluck( |
||||
1066 | array $array, |
||||
1067 | string|int $value, |
||||
1068 | string|int|null $key = null |
||||
1069 | ): array { |
||||
1070 | $results = []; |
||||
1071 | foreach ($array as $item) { |
||||
1072 | if (is_array($item)) { |
||||
1073 | $itemValue = static::get($item, $value); |
||||
1074 | |||||
1075 | // If the key is "null", we will just append the value to the array |
||||
1076 | // and keep looping. Otherwise we will key the array using |
||||
1077 | // the value of the key we received from the developer. |
||||
1078 | // Then we'll return the final array form. |
||||
1079 | if ($key === null) { |
||||
1080 | $results[] = $itemValue; |
||||
1081 | } else { |
||||
1082 | $itemKey = static::get($item, $key); |
||||
1083 | if (is_object($itemKey) && $itemKey instanceof Stringable) { |
||||
1084 | $itemKey = (string)$itemKey; |
||||
1085 | } |
||||
1086 | |||||
1087 | $results[$itemKey] = $itemValue; |
||||
1088 | } |
||||
1089 | } |
||||
1090 | } |
||||
1091 | |||||
1092 | return $results; |
||||
1093 | } |
||||
1094 | |||||
1095 | /** |
||||
1096 | * Collapse an array of arrays into a single array. |
||||
1097 | * @param array<mixed> $array |
||||
1098 | * @return array<mixed> |
||||
1099 | */ |
||||
1100 | public static function collapse(array $array): array |
||||
1101 | { |
||||
1102 | $results = []; |
||||
1103 | foreach ($array as $values) { |
||||
1104 | if (!is_array($values)) { |
||||
1105 | continue; |
||||
1106 | } |
||||
1107 | |||||
1108 | $results = array_merge($results, $values); |
||||
1109 | } |
||||
1110 | |||||
1111 | return $results; |
||||
1112 | } |
||||
1113 | |||||
1114 | /** |
||||
1115 | * Cross join the given arrays, returning all possible permutations. |
||||
1116 | * @param array<mixed> ...$arrays |
||||
1117 | * @return array<mixed> |
||||
1118 | */ |
||||
1119 | public static function crossJoin(array ...$arrays): array |
||||
1120 | { |
||||
1121 | $results = [[]]; |
||||
1122 | foreach ($arrays as $index => $array) { |
||||
1123 | $append = []; |
||||
1124 | |||||
1125 | foreach ($results as $product) { |
||||
1126 | foreach ($array as $item) { |
||||
1127 | $product[$index] = $item; |
||||
1128 | |||||
1129 | $append[] = $product; |
||||
1130 | } |
||||
1131 | } |
||||
1132 | |||||
1133 | $results = $append; |
||||
1134 | } |
||||
1135 | |||||
1136 | return $results; |
||||
1137 | } |
||||
1138 | |||||
1139 | /** |
||||
1140 | * |
||||
1141 | * @param array<int|string, mixed> $array |
||||
1142 | * @param mixed $value |
||||
1143 | * @param mixed $key |
||||
1144 | * @return array<mixed> |
||||
1145 | */ |
||||
1146 | public static function prepend(array $array, mixed $value, mixed $key = null): array |
||||
1147 | { |
||||
1148 | if ($key === null) { |
||||
1149 | array_unshift($array, $value); |
||||
1150 | } else { |
||||
1151 | $array = [$key => $value] + $array; |
||||
1152 | } |
||||
1153 | |||||
1154 | return $array; |
||||
1155 | } |
||||
1156 | |||||
1157 | /** |
||||
1158 | * Get one or a specified number of random values from an array. |
||||
1159 | * @param array<mixed> $array |
||||
1160 | * @param int|null $number |
||||
1161 | * |
||||
1162 | * @return mixed |
||||
1163 | */ |
||||
1164 | public static function random(array $array, ?int $number = null): mixed |
||||
1165 | { |
||||
1166 | $requested = $number === null ? 1 : $number; |
||||
1167 | $count = count($array); |
||||
1168 | |||||
1169 | if ($requested > $count) { |
||||
1170 | throw new InvalidArgumentException(sprintf( |
||||
1171 | 'You requested %d items, but there are only %d items available', |
||||
1172 | $requested, |
||||
1173 | $count |
||||
1174 | )); |
||||
1175 | } |
||||
1176 | |||||
1177 | if ($number === null) { |
||||
1178 | return $array[array_rand($array)]; |
||||
1179 | } |
||||
1180 | |||||
1181 | if ($number === 0) { |
||||
1182 | return []; |
||||
1183 | } |
||||
1184 | |||||
1185 | $keys = array_rand($array, $number); |
||||
1186 | |||||
1187 | $results = []; |
||||
1188 | foreach ((array)$keys as $key) { |
||||
1189 | $results[] = $array[$key]; |
||||
1190 | } |
||||
1191 | |||||
1192 | return $results; |
||||
1193 | } |
||||
1194 | |||||
1195 | /** |
||||
1196 | * Convert an array to string |
||||
1197 | * @param array<mixed> $array |
||||
1198 | * @param string $glue |
||||
1199 | * @return string |
||||
1200 | */ |
||||
1201 | public static function toString(array $array, string $glue = '_'): string |
||||
1202 | { |
||||
1203 | return implode($glue, $array); |
||||
1204 | } |
||||
1205 | |||||
1206 | /** |
||||
1207 | * Shuffle the given array and return the result. |
||||
1208 | * @param array<mixed> $array |
||||
1209 | * @param int|null $seed |
||||
1210 | * @return array<mixed> |
||||
1211 | */ |
||||
1212 | public static function shuffle(array $array, ?int $seed = null): array |
||||
1213 | { |
||||
1214 | if ($seed === null) { |
||||
1215 | shuffle($array); |
||||
1216 | } else { |
||||
1217 | mt_srand($seed); |
||||
1218 | shuffle($array); |
||||
1219 | mt_srand(); |
||||
1220 | } |
||||
1221 | |||||
1222 | return $array; |
||||
1223 | } |
||||
1224 | |||||
1225 | /** |
||||
1226 | * Normalize command line arguments like splitting "-abc" and "--xyz=...". |
||||
1227 | * @param array<int, string> $args |
||||
1228 | * @return array<string> |
||||
1229 | */ |
||||
1230 | public static function normalizeArguments(array $args): array |
||||
1231 | { |
||||
1232 | $normalized = []; |
||||
1233 | foreach ($args as $arg) { |
||||
1234 | if (preg_match('/^\-\w=/', $arg)) { |
||||
1235 | $normalized = array_merge( |
||||
1236 | $normalized, |
||||
1237 | (array)explode('=', $arg) |
||||
1238 | ); |
||||
1239 | } elseif (preg_match('/^\-\w{2,}/', $arg)) { |
||||
1240 | $splitArgs = implode(' -', str_split(ltrim($arg, '-'))); |
||||
0 ignored issues
–
show
It seems like
str_split(ltrim($arg, '-')) can also be of type boolean ; however, parameter $pieces of implode() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
1241 | $normalized = array_merge( |
||||
1242 | $normalized, |
||||
1243 | (array)explode(' ', '-' . $splitArgs) |
||||
1244 | ); |
||||
1245 | } elseif (preg_match('/^\-\-([^\s\=]+)\=/', $arg)) { |
||||
1246 | $normalized = array_merge( |
||||
1247 | $normalized, |
||||
1248 | (array)explode('=', $arg) |
||||
1249 | ); |
||||
1250 | } else { |
||||
1251 | $normalized[] = $arg; |
||||
1252 | } |
||||
1253 | } |
||||
1254 | |||||
1255 | return $normalized; |
||||
1256 | } |
||||
1257 | } |
||||
1258 |