ArrayUtil::prepareSortable()   F
last analyzed

Complexity

Conditions 14
Paths 440

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 44
rs 3.5364
cc 14
eloc 31
nc 440
nop 5

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sortedUniqueInts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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