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
|
|||||
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
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 For 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
![]() |
|||||
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
|
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
|
|||||
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
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
![]() |
|||||
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
|
|||||
949 | } |
||||
950 | } |
||||
951 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: