Passed
Push — master ( 4cb285...5a0531 )
by Mohamed
01:37
created

Collections::first()   A

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
c 0
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 3
nc 2
nop 2
crap 2
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 1
    public static function filter(array $array = [], Closure $closure = null): array
21
    {
22 1
        if ($closure) {
23 1
            $result = [];
24
25 1
            foreach ($array as $key => $value) {
26 1
                if (call_user_func($closure, $value)) {
27 1
                    $result[] = $value;
28
                }
29
            }
30
31 1
            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 9
    public static function first(array $array, $take = null)
49
    {
50 9
        if (!$take) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $take of type null|integer 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 8
            return array_shift($array);
52
        }
53
54 1
        return array_splice($array, 0, $take, true);
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
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...
65
     * @param null $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)) {
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type string; however, parameter $value of __::isNull() does only seem to accept 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

71
        if (__::isNull(/** @scrutinizer ignore-type */ $key)) {
Loading history...
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 8
                    $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 null|integer 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 8
        __::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
    public static function pluck($collection, string $property): array
192
    {
193 1
        $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 1
                        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
    public static function assign($collection1, $collection2)
286
    {
287
        return __::reduceRight(func_get_args(), function ($source, $result) {
288 2
            __::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 null $accumulator
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $accumulator is correct as it would always require null to be passed?
Loading history...
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 = __::first($collection);
0 ignored issues
show
Bug introduced by
It seems like $collection can also be of type object; however, parameter $array of __::first() 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 = __::first(/** @scrutinizer ignore-type */ $collection);
Loading history...
322
        }
323
324 7
        __::doForEachRight(
325 7
            $collection,
326 7
            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 null
348
     */
349 8
    public static function doForEachRight($collection, Closure $iterateFn)
350
    {
351 8
        return __::doForEach(__::iteratorReverse($collection), $iterateFn);
0 ignored issues
show
Bug introduced by
Are you sure the usage of __::doForEach(__::iterat...ollection), $iterateFn) targeting __::doForEach() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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

909
            $accumulator = __::first(/** @scrutinizer ignore-type */ $collection);
Loading history...
910
        }
911 5
        __::doForEach(
912 5
            $collection,
913 5
            function ($value, $key, $collection) use (&$accumulator, $iterateFn) {
914 5
                $accumulator = $iterateFn($accumulator, $value, $key, $collection);
915 5
            }
916
        );
917
918 5
        return $accumulator;
919
    }
920
921
    /**
922
     * Builds a multidimensional collection out of a hash map using the key as indicator where to put the value.
923
     *
924
     * @usage __::unease(['foo.bar' => 'ter', 'baz.0' => 'b', , 'baz.1' => 'z']);
925
     *        >> '['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]'
926
     *
927
     * @param array $collection hash map of values
928
     * @param string $separator the glue used in the keys
929
     *
930
     * @return array
931
     * @throws \Exception
932
     */
933 1
    public static function unease(array $collection, string $separator = '.'): array
934
    {
935 1
        $nonDefaultSeparator = $separator !== '.';
936 1
        $map = [];
937
938 1
        foreach ($collection as $key => $value) {
939 1
            $map = __::set(
940 1
                $map,
941 1
                $nonDefaultSeparator ? str_replace($separator, '.', $key) : $key,
942 1
                $value
943
            );
944
        }
945
946 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...
947
    }
948
}
949