Collections::first()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace __\Traits;
4
5
use __;
6
use Closure;
7
use Exception;
8
use stdClass;
9
10
trait Collections
11
{
12
    /**
13
     * Returns the values in the collection that pass the truth test.
14
     *
15
     * @param array    $array   array to filter
16
     * @param \Closure $closure closure to filter array based on
17
     *
18
     * @return array
19
     */
20 2
    public static function filter(array $array = [], Closure $closure = null): array
21
    {
22 2
        if ($closure) {
23 2
            $result = [];
24
25 2
            foreach ($array as $key => $value) {
26 2
                if (call_user_func($closure, $value)) {
27 2
                    $result[] = $value;
28
                }
29
            }
30
31 2
            return $result;
32
        }
33
34 1
        return __::compact($array);
35
    }
36
37
    /**
38
     * Gets the first element of an array. Passing n returns the first n elements.
39
     *
40
     * @usage __::first([1, 2, 3]);
41
     *        >> 1
42
     *
43
     * @param array    $array of values
44
     * @param int|null $take  number of values to return
45
     *
46
     * @return mixed
47
     */
48 8
    public static function first(array $array, $take = null)
49
    {
50 8
        if (!$take) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $take of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
51 6
            return array_shift($array);
52
        }
53
54 2
        return array_splice($array, 0, $take);
55
    }
56
57
    /**
58
     * Get item of an array by index, accepting nested index
59
     *
60
     * @usage __::get(['foo' => ['bar' => 'ter']], 'foo.bar');
61
     *        >> 'ter'
62
     *
63
     * @param array|object $collection array of values
64
     * @param null|string  $key        key or index
65
     * @param mixed        $default    default value to return if index not exist
66
     *
67
     * @return mixed
68
     */
69 15
    public static function get($collection = [], $key = null, $default = null)
70
    {
71 15
        if (__::isNull($key)) {
72 1
            return $collection;
73
        }
74
75 15
        if (!__::isObject($collection) && isset($collection[$key])) {
76 9
            return $collection[$key];
77
        }
78
79 8
        foreach (explode('.', $key) as $segment) {
80 8
            if (__::isObject($collection)) {
81 5
                if (!isset($collection->{$segment})) {
82 2
                    return $default instanceof Closure ? $default() : $default;
83
                } else {
84 5
                    $collection = $collection->{$segment};
85
                }
86
            } else {
87 3
                if (!isset($collection[$segment])) {
88 3
                    return $default instanceof Closure ? $default() : $default;
89
                } else {
90 3
                    $collection = $collection[$segment];
91
                }
92
            }
93
        }
94
95 8
        return $collection;
96
    }
97
98
    /**
99
     * Get last item(s) of an array
100
     *
101
     * @usage __::last([1, 2, 3, 4, 5], 2);
102
     *        >> [4, 5]
103
     *
104
     * @param array    $array array of values
105
     * @param int|null $take  number of returned values
106
     *
107
     * @return mixed
108
     */
109 1
    public static function last(array $array, $take = null)
110
    {
111 1
        if (!$take) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $take of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
112 1
            return array_pop($array);
113
        }
114
115 1
        return array_splice($array, -$take);
116
    }
117
118
    /**
119
     * Returns an array of values by mapping each in collection through the iterateFn. The iterateFn is invoked with
120
     * three arguments: (value, index|key, collection).
121
     *
122
     * @usage __::map([1, 2, 3], function($n) {
123
     *               return $n * 3;
124
     *           });
125
     *       >> [3, 6, 9]
126
     *
127
     * @param array|object $collection The collection of values to map over.
128
     * @param \Closure     $iterateFn  The function to apply on each value.
129
     *
130
     * @return array
131
     */
132 8
    public static function map($collection, Closure $iterateFn): array
133
    {
134 8
        $result = [];
135
136
        __::doForEach($collection, function ($value, $key, $collection) use (&$result, $iterateFn) {
137 8
            $result[] = $iterateFn($value, $key, $collection);
138 8
        });
139
140 8
        return $result;
141
    }
142
143
    /**
144
     * Returns the maximum value from the collection. If passed an iterator, max will return max value returned by the
145
     * iterator.
146
     *
147
     * @usage __::max([1, 2, 3]);
148
     *        >> 3
149
     *
150
     * @param array $array The array to iterate over
151
     *
152
     * @return mixed Returns the maximum value
153
     */
154 1
    public static function max(array $array = [])
155
    {
156 1
        return max($array);
157
    }
158
159
    /**
160
     * Returns the minimum value from the collection. If passed an iterator, min will return min value returned by the
161
     * iterator.
162
     *
163
     * @usage __::min([1, 2, 3]);
164
     *        >> 1
165
     *
166
     * @param array $array array of values
167
     *
168
     * @return mixed
169
     */
170 1
    public static function min(array $array = [])
171
    {
172 1
        return min($array);
173
    }
174
175
    /**
176
     * Returns an array of values belonging to a given property of each item in a collection.
177
     *
178
     * @usage $a = [
179
     *            ['foo' => 'bar',  'bis' => 'ter' ],
180
     *            ['foo' => 'bar2', 'bis' => 'ter2'],
181
     *        ];
182
     *
183
     *        __::pluck($a, 'foo');
184
     *        >> ['bar', 'bar2']
185
     *
186
     * @param array|object $collection array or object that can be converted to array
187
     * @param string       $property   property name
188
     *
189
     * @return array
190
     */
191 1
    public static function pluck($collection, string $property): array
192
    {
193
        $result = array_map(function ($value) use ($property) {
194 1
            if (is_array($value) && isset($value[$property])) {
195 1
                return $value[$property];
196 1
            } elseif (is_object($value) && isset($value->{$property})) {
197 1
                return $value->{$property};
198
            }
199 1
            foreach (__::split($property, '.') as $segment) {
200 1
                if (is_object($value)) {
201 1
                    if (isset($value->{$segment})) {
202 1
                        $value = $value->{$segment};
203
                    } else {
204 1
                        return null;
205
                    }
206
                } else {
207 1
                    if (isset($value[$segment])) {
208 1
                        $value = $value[$segment];
209
                    } else {
210
                        return null;
211
                    }
212
                }
213
            }
214
215 1
            return $value;
216 1
        }, (array)$collection);
217
218 1
        return array_values($result);
219
    }
220
221
222
    /**
223
     * Return data matching specific key value condition
224
     *
225
     * @usage __::where($a, ['age' => 16]);
226
     *        >> [['name' => 'maciej', 'age' => 16]]
227
     *
228
     * @param array $array    array of values
229
     * @param array $key      condition in format of ['KEY'=>'VALUE']
230
     * @param bool  $keepKeys keep original keys
231
     *
232
     * @return array
233
     */
234 1
    public static function where(array $array = [], array $key = [], bool $keepKeys = false): array
235
    {
236 1
        $result = [];
237
238 1
        foreach ($array as $k => $v) {
239 1
            $not = false;
240
241 1
            foreach ($key as $j => $w) {
242 1
                if (__::isArray($w)) {
243 1
                    $inKV = $v[$j] ?? [];
244 1
                    if (count(array_intersect_assoc($w, $inKV)) == 0) {
245 1
                        $not = true;
246 1
                        break;
247
                    }
248
                } else {
249 1
                    if (!isset($v[$j]) || $v[$j] != $w) {
250 1
                        $not = true;
251 1
                        break;
252
                    }
253
                }
254
            }
255
256 1
            if ($not == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
257 1
                if ($keepKeys) {
258 1
                    $result[$k] = $v;
259
                } else {
260 1
                    $result[] = $v;
261
                }
262
            }
263
        }
264
265 1
        return $result;
266
    }
267
268
    /**
269
     * Combines and merge collections provided with each others.
270
     *
271
     * If the collections have common keys, then the last passed keys override the
272
     * previous. If numerical indexes are passed, then last passed indexes override
273
     * the previous.
274
     *
275
     * For a recursive merge, see __::merge.
276
     *
277
     * @usage __::assign(['color' => ['favorite' => 'red', 5], 3], [10, 'color' => ['favorite' => 'green', 'blue']]);
278
     *        >> ['color' => ['favorite' => 'green', 'blue'], 10]
279
     *
280
     * @param array|object $collection1 Collection to assign to.
281
     * @param array|object $collection2 Other collections to assign
282
     *
283
     * @return array|object Assigned collection.
284
     */
285 2
    public static function assign($collection1, $collection2)
286
    {
287
        return __::reduceRight(func_get_args(), function ($source, $result) {
288
            __::doForEach($source, function ($sourceValue, $key) use (&$result) {
289 2
                $result = __::set($result, $key, $sourceValue);
290 2
            });
291
292 2
            return $result;
293 2
        }, []);
294
    }
295
296
    /**
297
     * Reduces $collection to a value which is the $accumulator result of running each
298
     * element in $collection - from right to left - thru $iterateFn, where each
299
     * successive invocation is supplied the return value of the previous.
300
     *
301
     * If $accumulator is not given, the first element of $collection is used as the
302
     * initial value.
303
     *
304
     * The $iterateFn is invoked with four arguments:
305
     * ($accumulator, $value, $index|$key, $collection).
306
     *
307
     * @usage __::reduceRight(['a', 'b', 'c'], function ($word, $char) {
308
     *                return $word . $char;
309
     *            }, '');
310
     *        >> 'cba'
311
     *
312
     * @param array|object $collection The collection to iterate over.
313
     * @param \Closure     $iterateFn  The function invoked per iteration.
314
     * @param mixed        $accumulator
315
     *
316
     * @return array|mixed|null (*): Returns the accumulated value.
317
     */
318 7
    public static function reduceRight($collection, Closure $iterateFn, $accumulator = null)
319
    {
320 7
        if ($accumulator === null) {
321 1
            $accumulator = array_pop($collection);
0 ignored issues
show
Bug introduced by
It seems like $collection can also be of type object; however, parameter $array of array_pop() 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 ignore-type  annotation

321
            $accumulator = array_pop(/** @scrutinizer ignore-type */ $collection);
Loading history...
322
        }
323
324 7
        __::doForEachRight(
325 7
            $collection,
326
            function ($value, $key, $collection) use (&$accumulator, $iterateFn) {
327 7
                $accumulator = $iterateFn($accumulator, $value, $key, $collection);
328 7
            }
329
        );
330
331 7
        return $accumulator;
332
    }
333
334
    /**
335
     * Iterate over elements of the collection, from right to left, and invokes iterate
336
     * for each element.
337
     *
338
     * The iterate is invoked with three arguments: (value, index|key, collection).
339
     * Iterate functions may exit iteration early by explicitly returning false.
340
     *
341
     * @usage __::doForEachRight([1, 2, 3], function ($value) { print_r($value) });
342
     *        >> (Side effect: print 3, 2, 1)
343
     *
344
     * @param array|object $collection The collection to iterate over.
345
     * @param \Closure     $iterateFn  The function to call for each value.
346
     *
347
     * @return boolean
348
     */
349 8
    public static function doForEachRight($collection, Closure $iterateFn)
350
    {
351 8
        __::doForEach(__::iteratorReverse($collection), $iterateFn);
0 ignored issues
show
Bug introduced by
It seems like $collection can also be of type object; however, parameter $iterable of __::iteratorReverse() 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 ignore-type  annotation

351
        __::doForEach(__::iteratorReverse(/** @scrutinizer ignore-type */ $collection), $iterateFn);
Loading history...
352 8
        return true;
353
    }
354
355
    /**
356
     * Iterate over elements of the collection and invokes iterate for each element.
357
     *
358
     * The iterate is invoked with three arguments: (value, index|key, collection).
359
     * Iterate functions may exit iteration early by explicitly returning false.
360
     *
361
     * @usage __::doForEach([1, 2, 3], function ($value) { print_r($value) });
362
     *        >> (Side effect: print 1, 2, 3)
363
     *
364
     * @param array|object $collection The collection to iterate over.
365
     * @param \Closure     $iterateFn  The function to call for each value
366
     *
367
     * @return boolean
368
     */
369 22
    public static function doForEach($collection, Closure $iterateFn)
370
    {
371 22
        foreach ($collection as $key => $value) {
372 22
            if ($iterateFn($value, $key, $collection) === false) {
373 4
                break;
374
            }
375
        }
376 22
        return true;
377
    }
378
379
    /**
380
     * @param array $iterable
381
     *
382
     * @return \Generator
383
     */
384 8
    public static function iteratorReverse($iterable)
385
    {
386 8
        for (end($iterable); ($key = key($iterable)) !== null; prev($iterable)) {
387 8
            yield $key => current($iterable);
388
        }
389 8
    }
390
391
    /**
392
     * Return a new collection with the item set at index to given value.
393
     * Index can be a path of nested indexes.
394
     *
395
     * If a portion of path doesn't exist, it's created. Arrays are created for missing
396
     * index in an array; objects are created for missing property in an object.
397
     *
398
     * @usage __::set(['foo' => ['bar' => 'ter']], 'foo.baz.ber', 'fer');
399
     *        >> '['foo' => ['bar' => 'ter', 'baz' => ['ber' => 'fer']]]'
400
     *
401
     * @param array|object    $collection collection of values
402
     * @param string|int|null $path       key or index
403
     * @param mixed           $value      the value to set at position $key
404
     *
405
     * @throws \Exception if the path consists of a non collection and strict is set to false
406
     *
407
     * @return array|object the new collection with the item set
408
     */
409 13
    public static function set($collection, $path, $value = null)
410
    {
411 13
        if ($path === null) {
412
            return $collection;
413
        }
414 13
        $portions = __::split($path, '.', 2);
415 13
        $key = $portions[0];
416 13
        if (count($portions) === 1) {
417 13
            return __::universalSet($collection, $key, $value);
418
        }
419
        // Here we manage the case where the portion of the path points to nothing,
420
        // or to a value that does not match the type of the source collection
421
        // (e.g. the path portion 'foo.bar' points to an integer value, while we
422
        // want to set a string at 'foo.bar.fun'. We first set an object or array
423
        //  - following the current collection type - to 'for.bar' before setting
424
        // 'foo.bar.fun' to the specified value).
425 6
        if (!__::has($collection, $key)
426 4
            || (__::isObject($collection) && !__::isObject(__::get($collection, $key)))
427 6
            || (__::isArray($collection) && !__::isArray(__::get($collection, $key)))
428
        ) {
429 6
            $collection = __::universalSet($collection, $key, __::isObject($collection) ? new stdClass : []);
430
        }
431
432 6
        return __::universalSet($collection, $key, __::set(__::get($collection, $key), $portions[1], $value));
433
    }
434
435
    /**
436
     * @param mixed $collection
437
     * @param mixed $key
438
     * @param mixed $value
439
     *
440
     * @return mixed
441
     */
442 13
    public static function universalSet($collection, $key, $value)
443
    {
444
        $set_object = function ($object, $key, $value) {
445 5
            $newObject = clone $object;
446 5
            $newObject->$key = $value;
447
448 5
            return $newObject;
449 13
        };
450
        $set_array = function ($array, $key, $value) {
451 8
            $array[$key] = $value;
452
453 8
            return $array;
454 13
        };
455 13
        $setter = __::isObject($collection) ? $set_object : $set_array;
456
457 13
        return call_user_func_array($setter, [$collection, $key, $value]);
458
    }
459
460
    /**
461
     * Returns if $input contains all requested $keys. If $strict is true it also checks if $input exclusively contains
462
     * the given $keys.
463
     *
464
     * @usage __::hasKeys(['foo' => 'bar', 'foz' => 'baz'], ['foo', 'foz']);
465
     *        >> true
466
     *
467
     * @param array   $collection of key values pairs
468
     * @param array   $keys       collection of keys to look for
469
     * @param boolean $strict     to exclusively check
470
     *
471
     * @return boolean
472
     */
473 2
    public static function hasKeys($collection = [], array $keys = [], bool $strict = false): bool
474
    {
475 2
        $keyCount = count($keys);
476 2
        if ($strict && count($collection) !== $keyCount) {
477 1
            return false;
478
        }
479
480 2
        return __::every(
481
            __::map($keys, function ($key) use ($collection) {
482 2
                return __::has($collection, $key);
483 2
            }),
484
            function ($v) {
485 2
                return $v === true;
486 2
            }
487
        );
488
    }
489
490
    /**
491
     * Return true if $collection contains the requested $key.
492
     *
493
     * In constraint to isset(), __::has() returns true if the key exists but is null.
494
     *
495
     * @usage __::has(['foo' => ['bar' => 'num'], 'foz' => 'baz'], 'foo.bar');
496
     *        >> true
497
     *
498
     *        __::hasKeys((object) ['foo' => 'bar', 'foz' => 'baz'], 'bar');
499
     *        >> false
500
     *
501
     * @param array|object $collection of key values pairs
502
     * @param string       $path       Path to look for.
503
     *
504
     * @return boolean
505
     */
506 11
    public static function has($collection, $path): bool
507
    {
508 11
        $portions = __::split($path, '.', 2);
509 11
        $key = $portions[0];
510
511 11
        if (count($portions) === 1) {
512 11
            return array_key_exists($key, (array)$collection);
513
        }
514
515 2
        return __::has(__::get($collection, $key), $portions[1]);
516
    }
517
518
    /**
519
     * Combines and concat collections provided with each others.
520
     *
521
     * If the collections have common keys, then the values are appended in an array.
522
     * If numerical indexes are passed, then values are appended.
523
     *
524
     * For a recursive merge, see __::merge.
525
     *
526
     * @usage __::concat(['color' => ['favorite' => 'red', 5], 3], [10, 'color' => ['favorite' => 'green', 'blue']]);
527
     *        >> ['color' => ['favorite' => ['green'], 5, 'blue'], 3, 10]
528
     *
529
     * @param array|object $collection1 Collection to assign to.
530
     * @param array|object $collection2 Other collections to assign.
531
     *
532
     * @return array|object Assigned collection.
533
     */
534 4
    public static function concat($collection1, $collection2)
535
    {
536 4
        $isObject = __::isObject($collection1);
537
538
        $args = __::map(func_get_args(), function ($arg) {
539 4
            return (array)$arg;
540 4
        });
541
542 4
        $merged = call_user_func_array('array_merge', $args);
543
544 4
        return $isObject ? (object)$merged : $merged;
545
    }
546
547
    /**
548
     * Recursively combines and concat collections provided with each others.
549
     *
550
     * If the collections have common keys, then the values are appended in an array.
551
     * If numerical indexes are passed, then values are appended.
552
     *
553
     * For a non-recursive concat, see __::concat.
554
     *
555
     * @usage __::concatDeep(['color' => ['favorite' => 'red', 5], 3], [10, 'color' => ['favorite' => 'green',
556
     *        'blue']]);
557
     *        >> ['color' => ['favorite' => ['red', 'green'], 5, 'blue'], 3, 10]
558
     *
559
     * @param array|object $collection1 First collection to concatDeep.
560
     * @param array|object $collection2 other collections to concatDeep.
561
     *
562
     * @return array|object Concatenated collection.
563
     */
564 2
    public static function concatDeep($collection1, $collection2)
565
    {
566
        return __::reduceRight(func_get_args(), function ($source, $result) {
567
            __::doForEach($source, function ($sourceValue, $key) use (&$result) {
568 2
                if (!__::has($result, $key)) {
569 2
                    $result = __::set($result, $key, $sourceValue);
570
                } else {
571 2
                    if (is_numeric($key)) {
572 2
                        $result = __::concat($result, [$sourceValue]);
573
                    } else {
574 2
                        $resultValue = __::get($result, $key);
575 2
                        $result = __::set($result, $key, __::concatDeep(
576 2
                            __::isCollection($resultValue) ? $resultValue : (array)$resultValue,
577 2
                            __::isCollection($sourceValue) ? $sourceValue : (array)$sourceValue
578
                        ));
579
                    }
580
                }
581 2
            });
582
583 2
            return $result;
584 2
        }, []);
585
    }
586
587
    /**
588
     * Flattens a complex collection by mapping each ending leafs value to a key consisting of all previous indexes.
589
     *
590
     * @usage __::ease(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]);
591
     *        >> '['foo.bar' => 'ter', 'baz.0' => 'b', , 'baz.1' => 'z']'
592
     *
593
     * @param array  $collection array of values
594
     * @param string $glue       glue between key path
595
     *
596
     * @return array flatten collection
597
     */
598 1
    public static function ease(array $collection, string $glue = '.'): array
599
    {
600 1
        $map = [];
601 1
        __::internalEase($map, $collection, $glue);
602
603 1
        return $map;
604
    }
605
606
    /**
607
     * Inner function for collections::ease
608
     *
609
     * @param array  $map
610
     * @param array  $array
611
     * @param string $glue
612
     * @param string $prefix
613
     */
614 1
    private static function internalEase(array &$map, array $array, string $glue, string $prefix = '')
615
    {
616 1
        foreach ($array as $index => $value) {
617 1
            if (is_array($value)) {
618 1
                __::internalEase($map, $value, $glue, $prefix . $index . $glue);
619
            } else {
620 1
                $map[$prefix . $index] = $value;
621
            }
622
        }
623 1
    }
624
625
    /**
626
     * Checks if predicate returns truthy for all elements of collection.
627
     *
628
     * Iteration is stopped once predicate returns falsey.
629
     * The predicate is invoked with three arguments: (value, index|key, collection).
630
     *
631
     * @usage __::every([1, 3, 4], function ($v) { return is_int($v); });
632
     *        >> true
633
     *
634
     * @param array|object $collection The collection to iterate over.
635
     * @param \Closure     $iterateFn  The function to call for each value.
636
     *
637
     * @return bool
638
     */
639 3
    public static function every($collection, Closure $iterateFn): bool
640
    {
641 3
        $truthy = true;
642
643 3
        __::doForEach(
644 3
            $collection,
645
            function ($value, $key, $collection) use (&$truthy, $iterateFn) {
646 3
                $truthy = $truthy && $iterateFn($value, $key, $collection);
647 3
                if (!$truthy) {
648 3
                    return false;
649
                }
650 3
            }
651
        );
652
653 3
        return $truthy;
654
    }
655
656
    /**
657
     * Returns an associative array where the keys are values of $key.
658
     *
659
     * @author Chauncey McAskill
660
     * @link   https://gist.github.com/mcaskill/baaee44487653e1afc0d array_group_by() function.
661
     *
662
     * @usage  __::groupBy([
663
     *                 ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
664
     *                 ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
665
     *                 ['state' => 'CA', 'city' => 'Mountain View', 'object' => 'Space pen'],
666
     *             ], 'state');
667
     *         >> [
668
     *              'IN' => [
669
     *                  ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
670
     *                  ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
671
     *              ],
672
     *              'CA' => [
673
     *                  ['state' => 'CA', 'city' => 'Mountain View', 'object' => 'Space pen']
674
     *              ]
675
     *            ]
676
     *
677
     *
678
     *         __::groupBy([
679
     *                 ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
680
     *                 ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'Manhole'],
681
     *                 ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
682
     *             ],
683
     *             function ($value) {
684
     *                 return $value->city;
685
     *             }
686
     *         );
687
     *         >> [
688
     *           'Indianapolis' => [
689
     *              ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
690
     *              ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'Manhole'],
691
     *           ],
692
     *           'San Diego' => [
693
     *              ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
694
     *           ]
695
     *         ]
696
     *
697
     * @param array $array
698
     * @param mixed $key
699
     *
700
     * @return array
701
     */
702 5
    public static function groupBy(array $array, $key): array
703
    {
704 5
        if (!is_bool($key) && !is_scalar($key) && !is_callable($key)) {
705
            return $array;
706
        }
707 5
        $grouped = [];
708 5
        foreach ($array as $value) {
709 5
            $groupKey = null;
710 5
            if (is_callable($key)) {
711 1
                $groupKey = call_user_func($key, $value);
712 4
            } elseif (is_object($value) && property_exists($value, (string)$key)) {
713 1
                $groupKey = $value->{$key};
714 3
            } elseif (is_array($value) && isset($value[$key])) {
715 3
                $groupKey = $value[$key];
716
            }
717 5
            if ($groupKey === null) {
718
                continue;
719
            }
720 5
            $grouped[$groupKey][] = $value;
721
        }
722 5
        if (($argCnt = func_num_args()) > 2) {
723 1
            $args = func_get_args();
724 1
            foreach ($grouped as $_key => $value) {
725 1
                $params = array_merge([$value], array_slice($args, 2, $argCnt));
726 1
                $grouped[$_key] = call_user_func_array('\__::groupBy', $params);
727
            }
728
        }
729
730 5
        return $grouped;
731
    }
732
733
    /**
734
     * Check if value is an empty array or object. We consider any non enumerable as empty.
735
     *
736
     * @usage __::isEmpty([]);
737
     *        >> true
738
     *
739
     * @param mixed $value The value to check for emptiness.
740
     *
741
     * @return bool
742
     */
743 1
    public static function isEmpty($value): bool
744
    {
745 1
        return (!__::isArray($value) && !__::isObject($value)) || count((array)$value) === 0;
746
    }
747
748
    /**
749
     * Transforms the keys in a collection by running each key through the iterator
750
     *
751
     * @param array    $array   array of values
752
     * @param \Closure $closure closure to map the keys
753
     *
754
     * @throws \Exception if closure doesn't return a valid key that can be used in PHP array
755
     *
756
     * @return array
757
     */
758 2
    public static function mapKeys(array $array, Closure $closure = null): array
759
    {
760 2
        if (is_null($closure)) {
761 1
            $closure = '__::identity';
762
        }
763 2
        $resultArray = [];
764 2
        foreach ($array as $key => $value) {
765 2
            $newKey = call_user_func_array($closure, [$key, $value, $array]);
766
            // key must be a number or string
767 2
            if (!is_numeric($newKey) && !is_string($newKey)) {
768 1
                throw new Exception('closure must returns a number or string');
769
            }
770 1
            $resultArray[$newKey] = $value;
771
        }
772
773 1
        return $resultArray;
774
    }
775
776
    /**
777
     * Transforms the values in a collection by running each value through the iterator
778
     *
779
     * @param array    $array   array of values
780
     * @param \Closure $closure closure to map the values
781
     *
782
     * @return array
783
     */
784 1
    public static function mapValues(array $array, Closure $closure = null): array
785
    {
786 1
        if (is_null($closure)) {
787 1
            $closure = '__::identity';
788
        }
789 1
        $resultArray = [];
790 1
        foreach ($array as $key => $value) {
791 1
            $resultArray[$key] = call_user_func_array($closure, [$value, $key, $array]);
792
        }
793
794 1
        return $resultArray;
795
    }
796
797
    /**
798
     * Recursively combines and merge collections provided with each others.
799
     *
800
     * If the collections have common keys, then the last passed keys override the previous.
801
     * If numerical indexes are passed, then last passed indexes override the previous.
802
     *
803
     * For a non-recursive merge, see __::merge.
804
     *
805
     * @usage __::merge(['color' => ['favorite' => 'red', 'model' => 3, 5], 3], [10, 'color' => ['favorite' => 'green',
806
     *        'blue']]);
807
     *        >> ['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10]
808
     *
809
     * @param array|object $collection1 First collection to merge.
810
     * @param array|object $collection2 Other collections to merge.
811
     *
812
     * @return array|object Concatenated collection.
813
     */
814 2
    public static function merge($collection1, $collection2)
815
    {
816
        return __::reduceRight(func_get_args(), function ($source, $result) {
817
            __::doForEach($source, function ($sourceValue, $key) use (&$result) {
818 2
                $value = $sourceValue;
819 2
                if (__::isCollection($value)) {
820 2
                    $value = __::merge(__::get($result, $key), $sourceValue);
821
                }
822 2
                $result = __::set($result, $key, $value);
823 2
            });
824
825 2
            return $result;
826 2
        }, []);
827
    }
828
829
    /**
830
     * Returns an array having only keys present in the given path list. Values for missing keys values will be filled
831
     * with provided default value.
832
     *
833
     * @usage __::pick(['a' => 1, 'b' => ['c' => 3, 'd' => 4]], ['a', 'b.d']);
834
     *        >> ['a' => 1, 'b' => ['d' => 4]]
835
     *
836
     * @param array|object $collection The collection to iterate over.
837
     * @param array        $paths      array paths to pick
838
     * @param null         $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
839
     *
840
     * @return array|object
841
     */
842 3
    public static function pick($collection = [], array $paths = [], $default = null)
843
    {
844
        return __::reduce($paths, function ($results, $path) use ($collection, $default) {
845 3
            return __::set($results, $path, __::get($collection, $path, $default));
846 3
        }, __::isObject($collection) ? new stdClass() : []);
0 ignored issues
show
Bug introduced by
It seems like __::isObject($collection...ew stdClass() : array() can also be of type stdClass; however, parameter $accumulator of __::reduce() does only seem to accept array|null, 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 ignore-type  annotation

846
        }, /** @scrutinizer ignore-type */ __::isObject($collection) ? new stdClass() : []);
Loading history...
847
    }
848
849
    /**
850
     * Reduces $collection to a value which is the $accumulator result of running each
851
     * element in $collection thru $iterateFn, where each successive invocation is supplied
852
     * the return value of the previous.
853
     *
854
     * If $accumulator is not given, the first element of $collection is used as the
855
     * initial value.
856
     *
857
     * The $iterateFn is invoked with four arguments:
858
     * ($accumulator, $value, $index|$key, $collection).
859
     *
860
     * @usage __::reduce([1, 2], function ($sum, $number) {
861
     *                return $sum + $number;
862
     *            }, 0);
863
     *        >> 3
864
     *
865
     *        $a = [
866
     *            ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
867
     *            ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'Manhole'],
868
     *            ['state' => 'IN', 'city' => 'Plainfield', 'object' => 'Basketball'],
869
     *            ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
870
     *            ['state' => 'CA', 'city' => 'Mountain View', 'object' => 'Space pen'],
871
     *        ];
872
     *        $iterateFn = function ($accumulator, $value) {
873
     *            if (isset($accumulator[$value['city']]))
874
     *                $accumulator[$value['city']]++;
875
     *            else
876
     *                $accumulator[$value['city']] = 1;
877
     *            return $accumulator;
878
     *        };
879
     *        __::reduce($c, $iterateFn, []);
880
     *        >> [
881
     *            'Indianapolis' => 2,
882
     *            'Plainfield' => 1,
883
     *            'San Diego' => 1,
884
     *            'Mountain View' => 1,
885
     *         ]
886
     *
887
     *        $object = new \stdClass();
888
     *        $object->a = 1;
889
     *        $object->b = 2;
890
     *        $object->c = 1;
891
     *        __::reduce($object, function ($result, $value, $key) {
892
     *            if (!isset($result[$value]))
893
     *                $result[$value] = [];
894
     *            $result[$value][] = $key;
895
     *            return $result;
896
     *        }, [])
897
     *        >> [
898
     *             '1' => ['a', 'c'],
899
     *             '2' => ['b']
900
     *         ]
901
     *
902
     * @param array      $collection The collection to iterate over.
903
     * @param \Closure   $iterateFn  The function invoked per iteration.
904
     * @param array|null $accumulator
905
     *
906
     * @return array|mixed|null (*): Returns the accumulated value.
907
     */
908 5
    public static function reduce($collection, Closure $iterateFn, $accumulator = null)
909
    {
910 5
        if ($accumulator === null) {
911 1
            $accumulator = array_shift($collection);
912
        }
913 5
        __::doForEach(
914 5
            $collection,
915
            function ($value, $key, $collection) use (&$accumulator, $iterateFn) {
916 5
                $accumulator = $iterateFn($accumulator, $value, $key, $collection);
917 5
            }
918
        );
919
920 5
        return $accumulator;
921
    }
922
923
    /**
924
     * Builds a multidimensional collection out of a hash map using the key as indicator where to put the value.
925
     *
926
     * @usage __::unease(['foo.bar' => 'ter', 'baz.0' => 'b', , 'baz.1' => 'z']);
927
     *        >> '['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]'
928
     *
929
     * @param array  $collection hash map of values
930
     * @param string $separator  the glue used in the keys
931
     *
932
     * @return array
933
     * @throws \Exception
934
     */
935 1
    public static function unease(array $collection, string $separator = '.'): array
936
    {
937 1
        $nonDefaultSeparator = $separator !== '.';
938 1
        $map = [];
939
940 1
        foreach ($collection as $key => $value) {
941 1
            $map = __::set(
942 1
                $map,
943 1
                $nonDefaultSeparator ? str_replace($separator, '.', $key) : $key,
944 1
                $value
945
            );
946
        }
947
948 1
        return $map;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $map could return the type object which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
949
    }
950
}
951