1: <?php
2:
3: /**
4: * Copyright 2015, Eduardo Trujillo
5: *
6: * For the full copyright and license information, please view the LICENSE
7: * file that was distributed with this source code.
8: *
9: * This file is part of the Nucleus package
10: */
11:
12: namespace Chromabits\Nucleus\Support;
13:
14: use Chromabits\Nucleus\Exceptions\CoreException;
15: use Chromabits\Nucleus\Exceptions\IndexOutOfBoundsException;
16: use Chromabits\Nucleus\Exceptions\LackOfCoffeeException;
17: use Chromabits\Nucleus\Foundation\StaticObject;
18: use Chromabits\Nucleus\Meditation\Arguments;
19: use Chromabits\Nucleus\Meditation\Boa;
20: use Chromabits\Nucleus\Meditation\Exceptions\InvalidArgumentException;
21:
22: /**
23: * Class Arr.
24: *
25: * @author Eduardo Trujillo <ed@chromabits.com>
26: * @package Chromabits\Nucleus\Support
27: */
28: class Arr extends StaticObject
29: {
30: /**
31: * Get the resulting value of an attempt to traverse a key path.
32: *
33: * Each key in the path is separated with a dot.
34: *
35: * For example, the following snippet should return `true`:
36: * ```php
37: * Arr::dotGet([
38: * 'hello' => [
39: * 'world' => true,
40: * ],
41: * ], 'hello.world');
42: * ```
43: *
44: * Additionally, a default value may be provided, which will be returned if
45: * the path does not yield to a value.
46: *
47: * @param array $array
48: * @param string $key
49: * @param null|mixed $default
50: *
51: * @return mixed
52: */
53: public static function dotGet(array $array, $key, $default = null)
54: {
55: if (is_null($key)) {
56: return $array;
57: }
58:
59: if (isset($array[$key])) {
60: return $array[$key];
61: }
62:
63: foreach (explode('.', $key) as $segment) {
64: if (!is_array($array) || !array_key_exists($segment, $array)) {
65: return Std::thunk($default);
66: }
67:
68: $array = $array[$segment];
69: }
70:
71: return $array;
72: }
73:
74: /**
75: * Set an array element using dot notation.
76: *
77: * Same as `Arr::dotGet()`, but the value is replaced instead of fetched.
78: *
79: * @param array $array
80: * @param string $key
81: * @param mixed $value
82: *
83: * @throws LackOfCoffeeException
84: */
85: public static function dotSet(array $array, $key, $value)
86: {
87: $path = explode('.', $key);
88: $total = count($path);
89:
90: $current = &$array;
91:
92: for ($ii = 0; $ii < $total; $ii++) {
93: if ($ii === $total - 1) {
94: $current[$path[$ii]] = $value;
95: } else {
96: if (!is_array($current)) {
97: throw new LackOfCoffeeException(
98: 'Part of the path is not an array.'
99: );
100: }
101:
102: if (!array_key_exists($path[$ii], $current)) {
103: $current[$path[$ii]] = [];
104: }
105:
106: $current = &$current[$path[$ii]];
107: }
108: }
109: }
110:
111: /**
112: * Like walk() but it uses a copy of the array instead.
113: *
114: * @param array $array
115: * @param callable $callback
116: * @param bool $recurse
117: * @param string $path
118: * @param bool $considerLeaves
119: *
120: * @return array
121: */
122: public static function walkCopy(
123: array $array,
124: callable $callback,
125: $recurse = false,
126: $path = '',
127: $considerLeaves = true
128: ) {
129: return static::walk(
130: $array,
131: $callback,
132: $recurse,
133: $path,
134: $considerLeaves
135: );
136: }
137:
138: /**
139: * A more complicated, but flexible, version of `array_walk`.
140: *
141: * This modified version is useful for flattening arrays without losing
142: * important structure data (how the array is arranged and nested).
143: *
144: * Possible applications: flattening complex validation responses or a
145: * configuration file.
146: *
147: * Additional features:
148: * - The current path in dot notation is provided to the callback.
149: * - Leaf arrays (no nested arrays) can be optionally ignored.
150: *
151: * Callback signature:
152: * ```php
153: * $callback($key, $value, $array, $path);
154: * ```
155: *
156: * @param array $array
157: * @param callable $callback
158: * @param bool $recurse
159: * @param string $path - The current path prefix.
160: * @param bool $considerLeaves
161: *
162: * @return array
163: */
164: public static function walk(
165: array &$array,
166: callable $callback,
167: $recurse = false,
168: $path = '',
169: $considerLeaves = true
170: ) {
171: $path = trim($path, '.');
172:
173: foreach ($array as $key => $value) {
174: if (is_array($value) && $recurse) {
175: if ($considerLeaves === false && !static::hasNested($value)) {
176: $callback($key, $value, $array, $path);
177: continue;
178: }
179:
180: $deeperPath = $key;
181:
182: if ($path !== '') {
183: $deeperPath = vsprintf('%s.%s', [$path, $key]);
184: }
185:
186: static::walk(
187: $array[$key],
188: $callback,
189: true,
190: $deeperPath,
191: $considerLeaves
192: );
193: continue;
194: }
195:
196: $callback($key, $value, $array, $path);
197: }
198:
199: return $array;
200: }
201:
202: /**
203: * Return whether or not an array has nested arrays.
204: *
205: * @param array $array
206: *
207: * @return bool
208: */
209: public static function hasNested(array $array)
210: {
211: foreach ($array as $value) {
212: if (is_array($value)) {
213: return true;
214: }
215: }
216:
217: return false;
218: }
219:
220: /**
221: * Return whether or not an array contains the specified key.
222: *
223: * @param array $input
224: * @param string|int $key
225: *
226: * @return bool
227: */
228: public static function has(array $input, $key)
229: {
230: return array_key_exists($key, $input);
231: }
232:
233: /**
234: * Get array elements that are not null.
235: *
236: * @param array $properties
237: * @param array|null $allowed
238: *
239: * @return array
240: */
241: public static function filterNullValues($properties, array $allowed = null)
242: {
243: // If provided, only use allowed properties
244: $properties = static::only($properties, $allowed);
245:
246: return array_filter(
247: $properties,
248: function ($value) {
249: return !is_null($value);
250: }
251: );
252: }
253:
254: /**
255: * Get an array with only the specified keys of the provided array.
256: *
257: * @param array $input
258: * @param array|null $included
259: *
260: * @return array
261: */
262: public static function only(array $input, $included = [])
263: {
264: Arguments::define(
265: Boa::either(
266: Boa::arrOf(Boa::either(
267: Boa::string(),
268: Boa::integer()
269: )),
270: Boa::null()
271: )
272: )->check($included);
273:
274: if (is_null($included)) {
275: return $input;
276: }
277:
278: if (count($included) == 0) {
279: return [];
280: }
281:
282: return array_intersect_key($input, array_flip($included));
283: }
284:
285: /**
286: * Filter the keys of an array to only the allowed set.
287: *
288: * @param array $input
289: * @param array $included
290: *
291: * @throws InvalidArgumentException
292: * @return array
293: * @deprecated
294: */
295: public static function filterKeys(array $input, $included = [])
296: {
297: if (is_null($included) || count($included) == 0) {
298: return $input;
299: }
300:
301: return array_intersect_key($input, array_flip($included));
302: }
303:
304: /**
305: * Get a copy of the provided array excluding the specified keys.
306: *
307: * @param array $input
308: * @param array $excluded
309: *
310: * @throws InvalidArgumentException
311: * @return array
312: */
313: public static function except(array $input, $excluded = [])
314: {
315: Arguments::define(Boa::arrOf(Boa::either(
316: Boa::string(),
317: Boa::integer()
318: )))->check($excluded);
319:
320: return Std::filter(function ($_, $key) use ($excluded) {
321: return !in_array($key, $excluded);
322: },
323: $input);
324: }
325:
326: /**
327: * Get a copy of the provided array excluding the specified values.
328: *
329: * @param array $input
330: * @param array $excluded
331: *
332: * @return array
333: * @throws InvalidArgumentException
334: */
335: public static function exceptValues(array $input, $excluded = [])
336: {
337: Arguments::define(Boa::arrOf(Boa::either(
338: Boa::string(),
339: Boa::integer()
340: )))->check($excluded);
341:
342: return Std::filter(function ($value, $_) use ($excluded) {
343: return !in_array($value, $excluded);
344: },
345: $input);
346: }
347:
348: /**
349: * Exchange two elements in an array.
350: *
351: * @param array $elements
352: * @param int $indexA
353: * @param int $indexB
354: *
355: * @throws IndexOutOfBoundsException
356: */
357: public static function exchange(array &$elements, $indexA, $indexB)
358: {
359: $count = count($elements);
360:
361: if (($indexA < 0 || $indexA > ($count - 1))
362: || $indexB < 0
363: || $indexB > ($count - 1)
364: ) {
365: throw new IndexOutOfBoundsException();
366: }
367:
368: $temp = $elements[$indexA];
369:
370: $elements[$indexA] = $elements[$indexB];
371:
372: $elements[$indexB] = $temp;
373: }
374:
375: /**
376: * Merge a vector of arrays performantly. Borrowed from libphutil.
377: * This has the same semantics as array_merge(), so these calls are
378: * equivalent:.
379: *
380: * array_merge($a, $b, $c);
381: * array_mergev(array($a, $b, $c));
382: *
383: * However, when you have a vector of arrays, it is vastly more performant
384: * to merge them with this function than by calling array_merge() in a loop,
385: * because using a loop generates an intermediary array on each iteration.
386: *
387: * @param array $arrayv
388: *
389: * @throws InvalidArgumentException
390: * @return array|mixed
391: */
392: public static function mergev(array $arrayv)
393: {
394: if (empty($arrayv)) {
395: return [];
396: }
397:
398: foreach ($arrayv as $key => $item) {
399: if (!is_array($item)) {
400: throw new InvalidArgumentException(
401: sprintf(
402: 'Expected all items passed to %s to be arrays, but ' .
403: 'argument with key "%s" has type "%s".',
404: __FUNCTION__ . '()',
405: $key,
406: gettype($item)));
407: }
408: }
409:
410: return call_user_func_array('array_merge', $arrayv);
411: }
412:
413: /**
414: * Get each key in an array.
415: *
416: * @param array $input
417: *
418: * @return array<string|int>
419: */
420: public static function keys(array $input)
421: {
422: return array_keys($input);
423: }
424:
425: /**
426: * Get the value of each element in the array.
427: *
428: * @param array $input
429: *
430: * @return array
431: */
432: public static function values(array $input)
433: {
434: return array_values($input);
435: }
436:
437: /**
438: * Extract the first element in an array.
439: *
440: * @param array $input
441: *
442: * @throws CoreException
443: * @return mixed
444: */
445: public static function head(array $input)
446: {
447: if (count($input) === 0) {
448: throw new CoreException('Empty array.');
449: }
450:
451: return $input[0];
452: }
453:
454: /**
455: * Extract the last element of an array.
456: *
457: * @param array $input
458: *
459: * @throws CoreException
460: * @return array
461: */
462: public static function tail(array $input)
463: {
464: if (count($input) === 0) {
465: throw new CoreException('Empty array.');
466: }
467:
468: return array_slice($input, 1);
469: }
470:
471: /**
472: * Extract the last element in an array.
473: *
474: * @param array $input
475: *
476: * @throws CoreException
477: * @return mixed
478: */
479: public static function last(array $input)
480: {
481: if (count($input) === 0) {
482: throw new CoreException('Empty array.');
483: }
484:
485: return $input[count($input) - 1];
486: }
487:
488: /**
489: * Extract all the elements of an array except the last one.
490: *
491: * @param array $input
492: *
493: * @throws CoreException
494: * @return array
495: */
496: public static function init(array $input)
497: {
498: if (count($input) === 0) {
499: throw new CoreException('Empty array.');
500: }
501:
502: return array_slice($input, 0, count($input) - 1);
503: }
504:
505: /**
506: * Return the provided array with all its elements in the inverse order.
507: *
508: * @param array $input
509: *
510: * @return array
511: */
512: public static function reverse(array $input)
513: {
514: return array_reverse($input);
515: }
516:
517: /**
518: * Returns the suffix of element after the first N elements.
519: *
520: * @param array $input
521: * @param int $count
522: *
523: * @return array
524: */
525: public static function drop(array $input, $count)
526: {
527: return array_slice($input, $count);
528: }
529:
530: /**
531: * Returns the index of the first occurrence of a value in the provided
532: * array.
533: *
534: * If the value is not found, false is returned.
535: *
536: * @param array $input
537: * @param mixed $value
538: *
539: * @return int|string|bool
540: */
541: public static function indexOf(array $input, $value)
542: {
543: foreach ($input as $key => $inputValue) {
544: if ($inputValue === $value) {
545: return $key;
546: }
547: }
548:
549: return false;
550: }
551:
552: /**
553: * Returns the suffix of elements before the first N elements
554: *
555: * @param array $input
556: * @param int $count
557: *
558: * @return array
559: */
560: public static function take(array $input, $count)
561: {
562: return array_slice($input, 0, $count);
563: }
564:
565: /**
566: * Merge two arrays together.
567: *
568: * @param array $base
569: * @param array $extension
570: *
571: * @return array
572: */
573: public static function merge(array $base, array $extension)
574: {
575: return array_merge($base, $extension);
576: }
577:
578: /**
579: * Check if an array contains the provided value.
580: *
581: * @param array $input
582: * @param mixed $value
583: *
584: * @return bool
585: */
586: public static function in(array $input, $value)
587: {
588: return in_array($value, $input);
589: }
590: }
591: