1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Oro\Component\PhpUtils; |
4
|
|
|
|
5
|
|
|
use Symfony\Component\PropertyAccess\PropertyPathInterface; |
6
|
|
|
|
7
|
|
|
use Oro\Component\PropertyAccess\PropertyAccessor; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
11
|
|
|
*/ |
12
|
|
|
class ArrayUtil |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* Checks whether the array is associative or sequential. |
16
|
|
|
* |
17
|
|
|
* @param array $array |
18
|
|
|
* |
19
|
|
|
* @return bool |
20
|
|
|
*/ |
21
|
|
|
public static function isAssoc(array $array) |
22
|
|
|
{ |
23
|
|
|
return array_values($array) !== $array; |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Sorts an array by specified property. |
28
|
|
|
* |
29
|
|
|
* This method uses the stable sorting algorithm. See http://en.wikipedia.org/wiki/Sorting_algorithm#Stability |
30
|
|
|
* Please use this method only if you really need stable sorting because this method is not so fast |
31
|
|
|
* as native PHP sort functions. |
32
|
|
|
* |
33
|
|
|
* @param array $array The array to be sorted |
34
|
|
|
* @param bool $reverse Indicates whether the sorting should be performed |
35
|
|
|
* in reverse order |
36
|
|
|
* @param mixed $propertyPath The property accessor. Can be string or PropertyPathInterface or callable |
37
|
|
|
* @param int $sortingFlags The sorting type. Can be SORT_NUMERIC or SORT_STRING |
38
|
|
|
* Also SORT_STRING can be combined with SORT_FLAG_CASE to sort |
39
|
|
|
* strings case-insensitively |
40
|
|
|
*/ |
41
|
|
|
public static function sortBy( |
42
|
|
|
array &$array, |
43
|
|
|
$reverse = false, |
44
|
|
|
$propertyPath = 'priority', |
45
|
|
|
$sortingFlags = SORT_NUMERIC |
46
|
|
|
) { |
47
|
|
|
if (empty($array)) { |
48
|
|
|
return; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* we have to implement such complex logic because the stable sorting is not supported in PHP for now |
53
|
|
|
* see https://bugs.php.net/bug.php?id=53341 |
54
|
|
|
*/ |
55
|
|
|
|
56
|
|
|
$stringComparison = 0 !== ($sortingFlags & SORT_STRING); |
57
|
|
|
$caseInsensitive = 0 !== ($sortingFlags & SORT_FLAG_CASE); |
58
|
|
|
|
59
|
|
|
$sortable = self::prepareSortable($array, $propertyPath, $reverse, $stringComparison, $caseInsensitive); |
60
|
|
|
if (!empty($sortable)) { |
61
|
|
|
$keys = self::getSortedKeys($sortable, $stringComparison, $reverse); |
62
|
|
|
|
63
|
|
|
$result = []; |
64
|
|
|
foreach ($keys as $key) { |
65
|
|
|
if (is_string($key)) { |
66
|
|
|
$result[$key] = $array[$key]; |
67
|
|
|
} else { |
68
|
|
|
$result[] = $array[$key]; |
69
|
|
|
} |
70
|
|
|
} |
71
|
|
|
$array = $result; |
72
|
|
|
} |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param mixed $a |
77
|
|
|
* @param mixed $b |
78
|
|
|
* @param bool $stringComparison |
79
|
|
|
* |
80
|
|
|
* @return int |
81
|
|
|
*/ |
82
|
|
|
private static function compare($a, $b, $stringComparison = false) |
83
|
|
|
{ |
84
|
|
|
if ($a === $b) { |
85
|
|
|
return 0; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
if ($stringComparison) { |
89
|
|
|
return strcmp($a, $b); |
90
|
|
|
} else { |
91
|
|
|
return $a < $b ? -1 : 1; |
92
|
|
|
} |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @param array $array |
97
|
|
|
* @param string|PropertyPathInterface|callable $propertyPath |
98
|
|
|
* @param bool $reverse |
99
|
|
|
* @param bool $stringComparison |
100
|
|
|
* @param bool $caseInsensitive |
101
|
|
|
* |
102
|
|
|
* @return array|null |
103
|
|
|
* |
104
|
|
|
* @SuppressWarnings(PHPMD.NPathComplexity) |
105
|
|
|
*/ |
106
|
|
|
private static function prepareSortable($array, $propertyPath, $reverse, $stringComparison, $caseInsensitive) |
107
|
|
|
{ |
108
|
|
|
$propertyAccessor = new PropertyAccessor(); |
109
|
|
|
$isSimplePropertyPath = is_string($propertyPath) && !preg_match('/.\[/', $propertyPath); |
110
|
|
|
$isCallback = is_callable($propertyPath); |
111
|
|
|
$defaultValue = $stringComparison ? '' : 0; |
112
|
|
|
$needSorting = $reverse; |
113
|
|
|
|
114
|
|
|
$result = []; |
115
|
|
|
$lastVal = null; |
116
|
|
|
$index = 0; |
117
|
|
|
foreach ($array as $key => $value) { |
118
|
|
|
if (is_array($value) && $isSimplePropertyPath) { |
119
|
|
|
// get array property directly to speed up |
120
|
|
|
$val = isset($value[$propertyPath]) || array_key_exists($propertyPath, $value) |
121
|
|
|
? $value[$propertyPath] |
122
|
|
|
: null; |
123
|
|
|
} elseif ($isCallback) { |
124
|
|
|
$val = call_user_func($propertyPath, $value); |
125
|
|
|
} else { |
126
|
|
|
$val = $propertyAccessor->getValue($value, $propertyPath); |
127
|
|
|
} |
128
|
|
|
if (null === $val) { |
129
|
|
|
$val = $defaultValue; |
130
|
|
|
} elseif ($caseInsensitive) { |
131
|
|
|
$val = strtolower($val); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$result[$key] = [$val, $index++]; |
135
|
|
|
|
136
|
|
|
if ($lastVal === null) { |
137
|
|
|
$lastVal = $val; |
138
|
|
|
} elseif (0 !== self::compare($lastVal, $val, $stringComparison)) { |
139
|
|
|
$lastVal = $val; |
140
|
|
|
$needSorting = true; |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
if (!$needSorting) { |
145
|
|
|
return null; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return $result; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @param array $sortable |
153
|
|
|
* @param bool $stringComparison |
154
|
|
|
* @param bool $reverse |
155
|
|
|
* |
156
|
|
|
* @return array |
157
|
|
|
*/ |
158
|
|
|
private static function getSortedKeys($sortable, $stringComparison, $reverse) |
159
|
|
|
{ |
160
|
|
|
uasort( |
161
|
|
|
$sortable, |
162
|
|
|
function ($a, $b) use ($stringComparison, $reverse) { |
163
|
|
|
$result = self::compare($a[0], $b[0], $stringComparison); |
164
|
|
|
if (0 === $result) { |
165
|
|
|
$result = self::compare($a[1], $b[1]); |
166
|
|
|
} elseif ($reverse) { |
167
|
|
|
$result = 0 - $result; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
return $result; |
171
|
|
|
} |
172
|
|
|
); |
173
|
|
|
|
174
|
|
|
return array_keys($sortable); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Compares 2 values based on order specified in the argument |
179
|
|
|
* |
180
|
|
|
* @param int[] $order |
181
|
|
|
* |
182
|
|
|
* @return callable |
183
|
|
|
*/ |
184
|
|
|
public static function createOrderedComparator(array $order) |
185
|
|
|
{ |
186
|
|
|
return function ($a, $b) use ($order) { |
187
|
|
|
if (!array_key_exists($b, $order)) { |
188
|
|
|
return -1; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
if (!array_key_exists($a, $order)) { |
192
|
|
|
return 1; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return $order[$a] - $order[$b]; |
196
|
|
|
}; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Return true if callback on any element returns truthy value, false otherwise |
201
|
|
|
* |
202
|
|
|
* @param callable $callback |
203
|
|
|
* @param array $array |
204
|
|
|
* |
205
|
|
|
* @return boolean |
206
|
|
|
*/ |
207
|
|
|
public static function some(callable $callback, array $array) |
208
|
|
|
{ |
209
|
|
|
foreach ($array as $item) { |
210
|
|
|
if (call_user_func($callback, $item)) { |
211
|
|
|
return true; |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
return false; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Return first element on which callback returns true value, null otherwise |
220
|
|
|
* |
221
|
|
|
* @param callable $callback |
222
|
|
|
* @param array $array |
223
|
|
|
* |
224
|
|
|
* @return mixed|null |
225
|
|
|
*/ |
226
|
|
|
public static function find(callable $callback, array $array) |
227
|
|
|
{ |
228
|
|
|
foreach ($array as $item) { |
229
|
|
|
if (call_user_func($callback, $item)) { |
230
|
|
|
return $item; |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return null; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Return copy of the array starting with item for which callback returns falsity value |
239
|
|
|
* |
240
|
|
|
* @param callable $callback |
241
|
|
|
* @param array $array |
242
|
|
|
* |
243
|
|
|
* @return array |
244
|
|
|
*/ |
245
|
|
|
public static function dropWhile(callable $callback, array $array) |
246
|
|
|
{ |
247
|
|
|
foreach ($array as $key => $value) { |
248
|
|
|
if (!call_user_func($callback, $value)) { |
249
|
|
|
return array_slice($array, $key); |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return []; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Recursively merge arrays. |
258
|
|
|
* |
259
|
|
|
* Merge two arrays as array_merge_recursive do, but instead of converting values to arrays when keys are same |
260
|
|
|
* replaces value from first array with value from second |
261
|
|
|
* |
262
|
|
|
* @param array $first |
263
|
|
|
* @param array $second |
264
|
|
|
* |
265
|
|
|
* @return array |
266
|
|
|
*/ |
267
|
|
|
public static function arrayMergeRecursiveDistinct(array $first, array $second) |
268
|
|
|
{ |
269
|
|
|
foreach ($second as $idx => $value) { |
270
|
|
|
if (is_integer($idx)) { |
271
|
|
|
$first[] = $value; |
272
|
|
|
} else { |
273
|
|
|
if (!array_key_exists($idx, $first)) { |
274
|
|
|
$first[$idx] = $value; |
275
|
|
|
} else { |
276
|
|
|
if (is_array($value)) { |
277
|
|
View Code Duplication |
if (is_array($first[$idx])) { |
278
|
|
|
$first[$idx] = self::arrayMergeRecursiveDistinct($first[$idx], $value); |
279
|
|
|
} else { |
280
|
|
|
$first[$idx] = $value; |
281
|
|
|
} |
282
|
|
|
} else { |
283
|
|
|
$first[$idx] = $value; |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return $first; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Return array of ranges (inclusive) |
294
|
|
|
* [[min1, max1], [min2, max2], ...] |
295
|
|
|
* |
296
|
|
|
* @param int[] $ints List of integers |
297
|
|
|
* |
298
|
|
|
* @return array |
299
|
|
|
*/ |
300
|
|
|
public static function intRanges(array $ints) |
301
|
|
|
{ |
302
|
|
|
$ints = array_unique($ints); |
303
|
|
|
sort($ints); |
304
|
|
|
|
305
|
|
|
$result = []; |
306
|
|
|
while (false !== ($subResult = static::shiftRange($ints))) { |
307
|
|
|
$result[] = $subResult; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $result; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* @param array $sortedUniqueInts |
315
|
|
|
* |
316
|
|
|
* @return array|false Array 2 elements [min, max] or false when the array is empty |
317
|
|
|
*/ |
318
|
|
|
public static function shiftRange(array &$sortedUniqueInts) |
319
|
|
|
{ |
320
|
|
|
if (!$sortedUniqueInts) { |
|
|
|
|
321
|
|
|
return false; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
$min = $max = reset($sortedUniqueInts); |
325
|
|
|
|
326
|
|
|
$c = 1; |
327
|
|
|
while (next($sortedUniqueInts) !== false && current($sortedUniqueInts) - $c === $min) { |
328
|
|
|
$max = current($sortedUniqueInts); |
329
|
|
|
array_shift($sortedUniqueInts); |
330
|
|
|
$c++; |
331
|
|
|
} |
332
|
|
|
array_shift($sortedUniqueInts); |
333
|
|
|
|
334
|
|
|
return [$min, $max]; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* Return the values from a single column in the input array |
339
|
|
|
* |
340
|
|
|
* http://php.net/manual/en/function.array-column.php |
341
|
|
|
* |
342
|
|
|
* @param array $array |
343
|
|
|
* @param mixed $columnKey |
344
|
|
|
* @param mixed $indexKey |
345
|
|
|
* |
346
|
|
|
* @return array |
347
|
|
|
*/ |
348
|
|
|
public static function arrayColumn(array $array, $columnKey, $indexKey = null) |
349
|
|
|
{ |
350
|
|
|
$result = []; |
351
|
|
|
|
352
|
|
|
if (empty($array)) { |
353
|
|
|
return []; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
if (empty($columnKey)) { |
357
|
|
|
throw new \InvalidArgumentException('Column key is empty'); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
foreach ($array as $item) { |
361
|
|
|
if (!isset($item[$columnKey])) { |
362
|
|
|
continue; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
if ($indexKey && !isset($item[$indexKey])) { |
366
|
|
|
continue; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
if ($indexKey) { |
370
|
|
|
$index = $item[$indexKey]; |
371
|
|
|
$result[$index] = $item[$columnKey]; |
372
|
|
|
} else { |
373
|
|
|
$result[] = $item[$columnKey]; |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
return $result; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* @param array $array |
382
|
|
|
* @param array $path |
383
|
|
|
* |
384
|
|
|
* @return array |
385
|
|
|
*/ |
386
|
|
|
public static function unsetPath(array $array, array $path) |
387
|
|
|
{ |
388
|
|
|
$key = array_shift($path); |
389
|
|
|
|
390
|
|
|
if (!$path) { |
|
|
|
|
391
|
|
|
unset($array[$key]); |
392
|
|
|
|
393
|
|
|
return $array; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
if (array_key_exists($key, $array) && is_array($array[$key])) { |
397
|
|
|
$array[$key] = static::unsetPath($array[$key], $path); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
return $array; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Returns the value in a nested associative array, |
405
|
|
|
* where $path is an array of keys. Returns $defaultValue if the key |
406
|
|
|
* is not present, or the not-found value if supplied. |
407
|
|
|
* |
408
|
|
|
* @param array $array |
409
|
|
|
* @param array $path |
410
|
|
|
* @param mixed $defaultValue |
411
|
|
|
* |
412
|
|
|
* @return mixed |
413
|
|
|
*/ |
414
|
|
|
public static function getIn(array $array, array $path, $defaultValue = null) |
415
|
|
|
{ |
416
|
|
|
$propertyPath = implode( |
417
|
|
|
'', |
418
|
|
|
array_map( |
419
|
|
|
function ($part) { |
420
|
|
|
return sprintf('[%s]', $part); |
421
|
|
|
}, |
422
|
|
|
$path |
423
|
|
|
) |
424
|
|
|
); |
425
|
|
|
|
426
|
|
|
$propertyAccessor = new PropertyAccessor(); |
427
|
|
|
if (!$propertyAccessor->isReadable($array, $propertyPath)) { |
428
|
|
|
return $defaultValue; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
return $propertyAccessor->getValue($array, $propertyPath); |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.