Passed
Push — master ( ec14da...7f38ec )
by Def
02:08
created

ArrayHelper::multisort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 21
rs 9.9
cc 2
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Helper;
6
7
use Closure;
8
9
/**
10
 * Short implementation of ArrayHelper from Yii2
11
 */
12
class ArrayHelper
13
{
14
    /**
15
     * Returns the values of a specified column in an array.
16
     * The input array should be multidimensional or an array of objects.
17
     *
18
     * For example,
19
     *
20
     * ```php
21
     * $array = [
22
     *     ['id' => '123', 'data' => 'abc'],
23
     *     ['id' => '345', 'data' => 'def'],
24
     * ];
25
     * $result = ArrayHelper::getColumn($array, 'id');
26
     * // the result is: ['123', '345']
27
     *
28
     * // using anonymous function
29
     * $result = ArrayHelper::getColumn($array, function ($element) {
30
     *     return $element['id'];
31
     * });
32
     * ```
33
     *
34
     * @param array $array
35
     * @param string $name
36
     *
37
     * @return array the list of column values
38
     */
39
    public static function getColumn(array $array, string $name): array
40
    {
41
        return array_map(
42
            static function (array|object $element) use ($name): mixed {
43
                return static::getValueByPath($element, $name);
44
            },
45
            $array
46
        );
47
    }
48
49
    /**
50
     * Retrieves the value of an array element or object property with the given key or property name.
51
     * If the key does not exist in the array, the default value will be returned instead.
52
     * Not used when getting value from an object.
53
     *
54
     * The key may be specified in a dot format to retrieve the value of a sub-array or the property
55
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
56
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
57
     * or `$array->x` is neither an array nor an object, the default value will be returned.
58
     * Note that if the array already has an element `x.y.z`, then its value will be returned
59
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
60
     * like `['x', 'y', 'z']`.
61
     *
62
     * Below are some usage examples,
63
     *
64
     * ```php
65
     * // working with array
66
     * $username = ArrayHelper::getValueByPath($_POST, 'username');
67
     * // working with object
68
     * $username = ArrayHelper::getValueByPath($user, 'username');
69
     * // working with anonymous function
70
     * $fullName = ArrayHelper::getValueByPath($user, function ($user, $defaultValue) {
71
     *     return $user->firstName . ' ' . $user->lastName;
72
     * });
73
     * // using dot format to retrieve the property of embedded object
74
     * $street = \yii\helpers\ArrayHelper::getValue($users, 'address.street');
75
     * // using an array of keys to retrieve the value
76
     * $value = \yii\helpers\ArrayHelper::getValue($versions, ['1.0', 'date']);
77
     * ```
78
     *
79
     * @param array|object $array array or object to extract value from
80
     * @param Closure|string $key key name of the array element, an array of keys or property name of the object,
81
     * or an anonymous function returning the value. The anonymous function signature should be:
82
     * `function($array, $defaultValue)`.
83
     * The possibility to pass an array of keys is available since version 2.0.4.
84
     * @param mixed|null $default the default value to be returned if the specified array key does not exist. Not used when
85
     * getting value from an object.
86
     *
87
     * @return mixed the value of the element if found, default value otherwise
88
     */
89
    public static function getValueByPath(object|array $array, Closure|string $key, mixed $default = null): mixed
90
    {
91
        if ($key instanceof Closure) {
92
            return $key($array, $default);
93
        }
94
95
        if (is_object($array) && property_exists($array, $key)) {
0 ignored issues
show
introduced by
The condition is_object($array) is always false.
Loading history...
96
            return $array->$key;
97
        }
98
99
        if (is_array($array) && array_key_exists($key, $array)) {
100
            return $array[$key];
101
        }
102
103
        if ($key && ($pos = strrpos($key, '.')) !== false) {
104
            /** @psalm-var array<string, mixed>|object $array */
105
            $array = static::getValueByPath($array, substr($key, 0, $pos), $default);
106
            $key = substr($key, $pos + 1);
107
        }
108
109
        if (is_object($array)) {
110
            // this is expected to fail if the property does not exist, or __get() is not implemented
111
            // it is not reliably possible to check whether a property is accessible beforehand
112
            return $array->$key;
113
        }
114
115
        if (array_key_exists($key, $array)) {
116
            return $array[$key];
117
        }
118
119
        return $default;
120
    }
121
122
    /**
123
     * Indexes and/or groups the array according to a specified key.
124
     * The input should be either multidimensional array or an array of objects.
125
     *
126
     * The $key can be either a key name of the sub-array, a property name of object, or an anonymous
127
     * function that must return the value that will be used as a key.
128
     *
129
     * $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based
130
     * on keys specified.
131
     *
132
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
133
     * to `$groups` not specified then the element is discarded.
134
     *
135
     * For example:
136
     *
137
     * ```php
138
     * $array = [
139
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
140
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
141
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
142
     * ];
143
     * $result = ArrayHelper::index($array, 'id');
144
     * ```
145
     *
146
     * The result will be an associative array, where the key is the value of `id` attribute
147
     *
148
     * ```php
149
     * [
150
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
151
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
152
     *     // The second element of an original array is overwritten by the last element because of the same id
153
     * ]
154
     * ```
155
     *
156
     * Passing `id` as a third argument will group `$array` by `id`:
157
     *
158
     * ```php
159
     * $result = ArrayHelper::index($array, null, 'id');
160
     * ```
161
     *
162
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
163
     * and indexed by `data` on the third level:
164
     *
165
     * ```php
166
     * [
167
     *     '123' => [
168
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
169
     *     ],
170
     *     '345' => [ // all elements with this index are present in the result array
171
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
172
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
173
     *     ]
174
     * ]
175
     * ```
176
     *
177
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
178
     * and indexed by the `data` on the third level:
179
     *
180
     * ```php
181
     * [
182
     *     '123' => [
183
     *         'laptop' => [
184
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
185
     *         ]
186
     *     ],
187
     *     '345' => [
188
     *         'tablet' => [
189
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
190
     *         ],
191
     *         'smartphone' => [
192
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
193
     *         ]
194
     *     ]
195
     * ]
196
     * ```
197
     *
198
     * @param array[] $array the array that needs to be indexed or grouped
199
     * @param string|null $key the column name or anonymous function which result will be used to index the array
200
     * @param string[] $groups the array of keys, that will be used to group the input array
201
     * by one or more keys. If the $key attribute or its value for the particular element is null and $groups is not
202
     * defined, the array element will be discarded. Otherwise, if $groups is specified, array element will be added
203
     * to the result array without any key. This parameter is available since version 2.0.8.
204
     *
205
     * @throws \Exception
206
     *
207
     * @return array the indexed and/or grouped array
208
     */
209
    public static function index(array $array, string|null $key = null, array $groups = []): array
210
    {
211
        $result = [];
212
        foreach ($array as $element) {
213
            $lastArray = &$result;
214
215
            foreach ($groups as $group) {
216
                /** @psalm-var string $value */
217
                $value = static::getValueByPath($element, $group);
218
                if (!array_key_exists($value, $lastArray)) {
219
                    $lastArray[$value] = [];
220
                }
221
                $lastArray = &$lastArray[$value];
222
            }
223
224
            if ($key === null) {
225
                if (!empty($groups)) {
226
                    $lastArray[] = $element;
227
                }
228
            } else {
229
                /** @psalm-var mixed $value */
230
                $value = static::getValueByPath($element, $key);
231
                if ($value !== null) {
232
                    $lastArray[(string)$value] = $element;
233
                }
234
            }
235
            unset($lastArray);
236
        }
237
238
        return $result;
239
    }
240
241
    /**
242
     * Returns a value indicating whether the given array is an associative array.
243
     *
244
     * An array is associative if all its keys are strings.
245
     *
246
     * Note that an empty array will NOT be considered associative.
247
     *
248
     * @param array $array the array being checked
249
     *
250
     * @return bool whether the array is associative
251
     */
252
    public static function isAssociative(array $array): bool
253
    {
254
        if (empty($array)) {
255
            return false;
256
        }
257
258
        foreach ($array as $key => $_value) {
259
            if (is_string($key)) {
260
                return true;
261
            }
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * Sorts an array of objects or arrays (with the same structure) by one or several keys.
269
     *
270
     * @param array $array the array to be sorted. The array will be modified after calling this method.
271
     * @param string $key the key(s) to be sorted by.
272
     */
273
    public static function multisort(
274
        array &$array,
275
        string $key
276
    ): void {
277
        if (empty($array)) {
278
            return;
279
        }
280
281
        $column = static::getColumn($array, $key);
282
283
        array_multisort(
284
            $column,
285
            SORT_ASC,
0 ignored issues
show
Bug introduced by
Yiisoft\Db\Helper\SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

285
            /** @scrutinizer ignore-type */ SORT_ASC,
Loading history...
286
            SORT_NUMERIC,
0 ignored issues
show
Bug introduced by
Yiisoft\Db\Helper\SORT_NUMERIC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

286
            /** @scrutinizer ignore-type */ SORT_NUMERIC,
Loading history...
287
288
            // This fix is used for cases when main sorting specified by columns has equal values
289
            // Without it will lead to Fatal Error: Nesting level too deep - recursive dependency?
290
            range(1, count($array)),
0 ignored issues
show
Bug introduced by
range(1, count($array)) cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

290
            /** @scrutinizer ignore-type */ range(1, count($array)),
Loading history...
291
            SORT_ASC,
292
            SORT_NUMERIC,
293
            $array
294
        );
295
    }
296
}
297