1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
/* |
5
|
|
|
* This file is part of Underscore.php |
6
|
|
|
* |
7
|
|
|
* (c) Maxime Fabre <[email protected]> |
8
|
|
|
* |
9
|
|
|
* For the full copyright and license information, please view the LICENSE |
10
|
|
|
* file that was distributed with this source code. |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Underscore\Methods; |
14
|
|
|
|
15
|
|
|
use Closure; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Abstract Collection type |
19
|
|
|
* Methods that apply to both objects and arrays. |
20
|
|
|
*/ |
21
|
|
|
abstract class CollectionMethods |
22
|
|
|
{ |
23
|
|
|
//////////////////////////////////////////////////////////////////// |
24
|
|
|
///////////////////////////// ANALYZE ////////////////////////////// |
25
|
|
|
//////////////////////////////////////////////////////////////////// |
26
|
|
|
/** |
27
|
|
|
* Check if an array has a given key. |
28
|
|
|
* |
29
|
|
|
* @param array $array |
30
|
|
|
* |
31
|
|
|
*/ |
32
|
|
|
public static function has(mixed $array, string $key) : bool |
33
|
|
|
{ |
34
|
9 |
|
// Generate unique string to use as marker |
35
|
|
|
$unfound = StringsMethods::random(5); |
36
|
|
|
|
37
|
9 |
|
return static::get($array, $key, $unfound) !== $unfound; |
38
|
|
|
} |
39
|
9 |
|
|
40
|
|
|
//////////////////////////////////////////////////////////////////// |
41
|
|
|
//////////////////////////// FETCH FROM //////////////////////////// |
42
|
|
|
//////////////////////////////////////////////////////////////////// |
43
|
|
|
/** |
44
|
|
|
* Get a value from an collection using dot-notation. |
45
|
|
|
* |
46
|
|
|
* @param array $collection The collection to get from |
47
|
|
|
* @param string|int|null $key The key to look for |
48
|
|
|
* @param mixed $default Default value to fallback to |
49
|
|
|
*/ |
50
|
|
|
public static function get(mixed $collection, mixed $key = null, mixed $default = null) : mixed |
51
|
|
|
{ |
52
|
|
|
if ($key === null) { |
53
|
|
|
return $collection; |
54
|
|
|
} |
55
|
60 |
|
|
56
|
|
|
$collection = (array) $collection; |
57
|
60 |
|
|
58
|
1 |
|
if (isset($collection[$key])) { |
59
|
|
|
return $collection[$key]; |
60
|
|
|
} |
61
|
60 |
|
|
62
|
|
|
// Crawl through collection, get key according to object or not |
63
|
60 |
|
foreach (explode('.', (string) $key) as $segment) { |
64
|
24 |
|
$collection = (array) $collection; |
65
|
|
|
|
66
|
|
|
if (!isset($collection[$segment])) { |
67
|
|
|
return $default instanceof Closure ? $default() : $default; |
68
|
51 |
|
} |
69
|
51 |
|
|
70
|
|
|
$collection = $collection[$segment]; |
71
|
51 |
|
} |
72
|
47 |
|
|
73
|
|
|
return $collection; |
74
|
|
|
} |
75
|
36 |
|
|
76
|
|
|
/** |
77
|
|
|
* Set a value in a collection using dot notation. |
78
|
6 |
|
* |
79
|
|
|
* @param mixed $collection The collection |
80
|
|
|
* @param string $key The key to set |
81
|
|
|
* @param mixed $value Its value |
82
|
|
|
*/ |
83
|
|
|
public static function set(mixed $collection, string $key, mixed $value) : mixed |
84
|
|
|
{ |
85
|
|
|
static::internalSet($collection, $key, $value); |
86
|
|
|
|
87
|
|
|
return $collection; |
88
|
|
|
} |
89
|
|
|
|
90
|
11 |
|
/** |
91
|
|
|
* Get a value from a collection and set it if it wasn't. |
92
|
11 |
|
* |
93
|
|
|
* @param mixed $collection The collection |
94
|
11 |
|
* @param string $key The key |
95
|
|
|
* @param mixed $default The default value to set if it isn't |
96
|
|
|
*/ |
97
|
|
|
public static function setAndGet(mixed &$collection, string $key, mixed $default = null) : mixed |
98
|
|
|
{ |
99
|
|
|
// If the key doesn't exist, set it |
100
|
|
|
if (!static::has($collection, $key)) { |
101
|
|
|
$collection = static::set($collection, $key, $default); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
return static::get($collection, $key); |
105
|
|
|
} |
106
|
5 |
|
|
107
|
|
|
/** |
108
|
|
|
* Remove a value from an array using dot notation. |
109
|
5 |
|
* |
110
|
5 |
|
* |
111
|
|
|
*/ |
112
|
|
|
public static function remove(mixed $collection, string|array $key) : mixed |
113
|
5 |
|
{ |
114
|
|
|
// Recursive call |
115
|
|
|
if (\is_array($key)) { |
|
|
|
|
116
|
|
|
foreach ($key as $k) { |
117
|
|
|
static::internalRemove($collection, $k); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return $collection; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
static::internalRemove($collection, $key); |
124
|
5 |
|
|
125
|
|
|
return $collection; |
126
|
|
|
} |
127
|
5 |
|
|
128
|
1 |
|
/** |
129
|
1 |
|
* Fetches all columns $property from a multimensionnal array. |
130
|
|
|
* |
131
|
|
|
* @param $collection |
132
|
1 |
|
* @param $property |
133
|
|
|
*/ |
134
|
|
|
public static function pluck($collection, $property) : object|array |
135
|
4 |
|
{ |
136
|
|
|
$plucked = array_map(fn($value): mixed => ArraysMethods::get($value, $property), (array) $collection); |
137
|
4 |
|
|
138
|
|
|
// Convert back to object if necessary |
139
|
|
|
if (\is_object($collection)) { |
140
|
|
|
return (object) $plucked; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return $plucked; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Filters an array of objects (or a numeric array of associative arrays) based on the value of a particular |
148
|
3 |
|
* property within that. |
149
|
|
|
* |
150
|
|
|
* @param $collection |
151
|
3 |
|
* @param string|null $comparisonOp |
152
|
3 |
|
* |
153
|
|
|
*/ |
154
|
|
|
public static function filterBy($collection, string $property, mixed $value, string $comparisonOp = null) : |
155
|
3 |
|
object|array |
156
|
1 |
|
{ |
157
|
|
|
if (!$comparisonOp) { |
158
|
|
|
$comparisonOp = \is_array($value) ? 'contains' : 'eq'; |
159
|
3 |
|
} |
160
|
|
|
|
161
|
|
|
$ops = [ |
162
|
|
|
'eq' => fn($item, $prop, $value) : bool => $item[$prop] === $value, |
163
|
|
|
'gt' => fn($item, $prop, $value) : bool => $item[$prop] > $value, |
164
|
|
|
'gte' => fn($item, $prop, $value) : bool => $item[$prop] >= $value, |
165
|
|
|
'lt' => fn($item, $prop, $value) : bool => $item[$prop] < $value, |
166
|
|
|
'lte' => fn($item, $prop, $value) : bool => $item[$prop] <= $value, |
167
|
|
|
'ne' => fn($item, $prop, $value) : bool => $item[$prop] !== $value, |
168
|
|
|
'contains' => fn($item, $prop, $value) : bool => \in_array($item[$prop], (array) $value, true), |
169
|
|
|
'notContains' => fn($item, $prop, $value) : bool => ! \in_array($item[$prop], (array) $value, true), |
170
|
|
|
'newer' => fn( |
171
|
|
|
$item, |
172
|
|
|
$prop, |
173
|
4 |
|
$value |
174
|
|
|
) : bool => strtotime((string) $item[$prop]) > strtotime((string) $value), |
175
|
4 |
|
'older' => fn( |
176
|
2 |
|
$item, |
177
|
|
|
$prop, |
178
|
|
|
$value |
179
|
|
|
) : bool => strtotime((string) $item[$prop]) < strtotime((string) $value), |
180
|
4 |
|
]; |
181
|
4 |
|
$result = array_values(array_filter((array) $collection, function ($item) use ( |
182
|
|
|
$property, |
183
|
|
|
$value, |
184
|
4 |
|
$ops, |
185
|
|
|
$comparisonOp |
186
|
|
|
): bool { |
187
|
4 |
|
$item = (array) $item; |
188
|
|
|
$item[$property] = static::get($item, $property, []); |
189
|
4 |
|
|
190
|
4 |
|
return $ops[$comparisonOp]($item, $property, $value); |
191
|
|
|
})); |
192
|
|
|
if (\is_object($collection)) { |
193
|
4 |
|
return (object) $result; |
194
|
|
|
} |
195
|
1 |
|
|
196
|
4 |
|
return $result; |
197
|
|
|
} |
198
|
1 |
|
|
199
|
4 |
|
/** |
200
|
|
|
* @param $collection |
201
|
|
|
* @param $property |
202
|
4 |
|
* @param $value |
203
|
|
|
* |
204
|
|
|
* @return array|mixed |
205
|
4 |
|
*/ |
206
|
|
|
public static function findBy($collection, string $property, $value, string $comparisonOp = 'eq'): mixed |
207
|
1 |
|
{ |
208
|
4 |
|
$filtered = static::filterBy($collection, $property, $value, $comparisonOp); |
209
|
|
|
|
210
|
|
|
return ArraysMethods::first(\is_array($filtered) ? $filtered : (array) $filtered); |
211
|
4 |
|
} |
212
|
4 |
|
|
213
|
4 |
|
//////////////////////////////////////////////////////////////////// |
214
|
4 |
|
///////////////////////////// ANALYZE ////////////////////////////// |
215
|
|
|
//////////////////////////////////////////////////////////////////// |
216
|
4 |
|
/** |
217
|
4 |
|
* Get all keys from a collection. |
218
|
|
|
* |
219
|
4 |
|
* @param $collection |
220
|
4 |
|
*/ |
221
|
4 |
|
public static function keys($collection) : array |
222
|
|
|
{ |
223
|
|
|
return array_keys((array) $collection); |
224
|
|
|
} |
225
|
4 |
|
|
226
|
|
|
/** |
227
|
|
|
* Get all values from a collection. |
228
|
|
|
* |
229
|
|
|
* @param $collection |
230
|
|
|
*/ |
231
|
|
|
public static function values($collection) : array |
232
|
|
|
{ |
233
|
|
|
return array_values((array) $collection); |
234
|
|
|
} |
235
|
|
|
|
236
|
2 |
|
//////////////////////////////////////////////////////////////////// |
237
|
|
|
////////////////////////////// ALTER /////////////////////////////// |
238
|
2 |
|
//////////////////////////////////////////////////////////////////// |
239
|
|
|
/** |
240
|
2 |
|
* Replace a key with a new key/value pair. |
241
|
|
|
* |
242
|
|
|
* |
243
|
|
|
*/ |
244
|
|
|
public static function replace(array|object $collection, string $replace, string $key, mixed $value) : mixed |
245
|
|
|
{ |
246
|
|
|
$collection = static::remove($collection, $replace); |
247
|
|
|
|
248
|
|
|
return static::set($collection, $key, $value); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Sort a collection by value, by a closure or by a property |
253
|
|
|
* If the sorter is null, the collection is sorted naturally. |
254
|
2 |
|
* |
255
|
|
|
* @param string|callable|null $sorter |
256
|
2 |
|
* |
257
|
|
|
*/ |
258
|
|
|
public static function sort(array|object $collection, string|callable $sorter = null, string $direction = 'asc') : |
259
|
|
|
array |
260
|
|
|
{ |
261
|
|
|
$collection = (array) $collection; |
262
|
|
|
|
263
|
|
|
// Get correct PHP constant for direction |
264
|
|
|
$directionNumber = (strtolower($direction) === 'desc') ? SORT_DESC : SORT_ASC; |
265
|
|
|
|
266
|
2 |
|
// Transform all values into their results |
267
|
|
|
if ($sorter !== null) { |
268
|
2 |
|
$results = ArraysMethods::each( |
269
|
|
|
$collection, |
270
|
|
|
fn($value) => \is_callable($sorter) ? $sorter($value) : ArraysMethods::get($value, $sorter) |
271
|
|
|
); |
272
|
|
|
} else { |
273
|
|
|
$results = $collection; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
// Sort by the results and replace by original values |
277
|
|
|
array_multisort($results, $directionNumber, SORT_REGULAR, $collection); |
|
|
|
|
278
|
|
|
|
279
|
|
|
return $collection; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Group values from a collection according to the results of a closure. |
284
|
|
|
* |
285
|
2 |
|
* @param $collection |
286
|
|
|
* @param $grouper |
287
|
2 |
|
* |
288
|
2 |
|
*/ |
289
|
|
|
public static function group(mixed $collection, callable|string $grouper, bool $saveKeys = false) : array |
290
|
2 |
|
{ |
291
|
|
|
$collection = (array) $collection; |
292
|
|
|
$result = []; |
293
|
|
|
|
294
|
|
|
// Iterate over values, group by property/results from closure |
295
|
|
|
foreach ($collection as $key => $value) { |
296
|
|
|
$groupKey = \is_callable($grouper) ? $grouper($value, $key) : ArraysMethods::get($value, $grouper); |
297
|
|
|
$newValue = static::get($result, $groupKey); |
298
|
|
|
|
299
|
|
|
// Add to results |
300
|
|
|
if ($groupKey !== null && $saveKeys) { |
301
|
|
|
$result[$groupKey] = $newValue; |
302
|
|
|
$result[$groupKey][$key] = $value; |
303
|
2 |
|
} elseif ($groupKey !== null) { |
304
|
|
|
$result[$groupKey] = $newValue; |
305
|
2 |
|
$result[$groupKey][] = $value; |
306
|
|
|
} |
307
|
|
|
} |
308
|
2 |
|
|
309
|
|
|
return $result; |
310
|
|
|
} |
311
|
2 |
|
|
312
|
|
|
//////////////////////////////////////////////////////////////////// |
313
|
2 |
|
////////////////////////////// HELPERS ///////////////////////////// |
314
|
2 |
|
//////////////////////////////////////////////////////////////////// |
315
|
|
|
/** |
316
|
1 |
|
* Internal mechanic of set method. |
317
|
|
|
* |
318
|
|
|
* @param $collection |
319
|
|
|
* @param $key |
320
|
2 |
|
* @param $value |
321
|
|
|
*/ |
322
|
2 |
|
protected static function internalSet(&$collection, $key, $value) : mixed |
323
|
|
|
{ |
324
|
|
|
if ($key === null) { |
325
|
|
|
return $collection = $value; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
// Explode the keys |
329
|
|
|
$keys = explode('.', (string) $key); |
330
|
|
|
|
331
|
|
|
// Crawl through the keys |
332
|
|
|
while (\count($keys) > 1) { |
333
|
|
|
$key = array_shift($keys); |
334
|
3 |
|
|
335
|
|
|
// If we're dealing with an object |
336
|
3 |
|
if (\is_object($collection)) { |
337
|
3 |
|
$collection->{$key} = static::get($collection, $key, []); |
338
|
|
|
$collection = &$collection->{$key}; |
339
|
|
|
// If we're dealing with an array |
340
|
3 |
|
} else { |
341
|
3 |
|
$collection[$key] = static::get($collection, $key, []); |
342
|
3 |
|
$collection = &$collection[$key]; |
343
|
|
|
} |
344
|
|
|
} |
345
|
3 |
|
|
346
|
1 |
|
// Bind final tree on the collection |
347
|
1 |
|
$key = array_shift($keys); |
348
|
2 |
|
if (\is_array($collection)) { |
349
|
1 |
|
$collection[$key] = $value; |
350
|
1 |
|
} else { |
351
|
|
|
$collection->{$key} = $value; |
352
|
|
|
} |
353
|
|
|
|
354
|
3 |
|
return $collection; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* Internal mechanics of remove method. |
359
|
|
|
* |
360
|
|
|
* @param string $key |
361
|
|
|
* |
362
|
|
|
*/ |
363
|
|
|
protected static function internalRemove(array|object &$collection, mixed $key) : bool |
364
|
|
|
{ |
365
|
|
|
// Explode keys |
366
|
|
|
$keys = explode('.', $key); |
367
|
|
|
|
368
|
|
|
// Crawl though the keys |
369
|
|
|
while (\count($keys) > 1) { |
370
|
11 |
|
$key = array_shift($keys); |
371
|
|
|
|
372
|
11 |
|
if ( ! static::has($collection, $key)) { |
373
|
|
|
return false; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
// If we're dealing with an object |
377
|
11 |
|
if (\is_object($collection)) { |
378
|
|
|
$collection = &$collection->{$key}; |
379
|
|
|
// If we're dealing with an array |
380
|
11 |
|
} else { |
381
|
2 |
|
$collection = &$collection[$key]; |
382
|
|
|
} |
383
|
|
|
} |
384
|
2 |
|
|
385
|
1 |
|
$key = array_shift($keys); |
386
|
1 |
|
if (\is_object($collection)) { |
387
|
|
|
unset($collection->{$key}); |
388
|
|
|
} else { |
389
|
|
|
unset($collection[$key]); |
390
|
2 |
|
} |
391
|
2 |
|
|
392
|
|
|
return true; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
11 |
|
* Given a list, and an iteratee function that returns |
397
|
11 |
|
* a key for each element in the list (or a property name), |
398
|
8 |
|
* returns an object with an index of each item. |
399
|
|
|
* Just like groupBy, but for when you know your keys are unique. |
400
|
|
|
*/ |
401
|
3 |
|
public static function indexBy(array $array, mixed $key) : array |
402
|
|
|
{ |
403
|
11 |
|
$results = []; |
404
|
|
|
|
405
|
|
|
foreach ($array as $a) { |
406
|
|
|
if (isset($a[$key])) { |
407
|
|
|
$results[$a[$key]] = $a; |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
return $results; |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|