Passed
Pull Request — master (#479)
by Def
02:11
created

ArrayHelper::getValueByPath()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 14
c 3
b 1
f 0
dl 0
loc 31
rs 7.6666
cc 10
nc 9
nop 3

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

289
            /** @scrutinizer ignore-type */ SORT_ASC,
Loading history...
290
            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

290
            /** @scrutinizer ignore-type */ SORT_NUMERIC,
Loading history...
291
292
            // This fix is used for cases when main sorting specified by columns has equal values
293
            // Without it will lead to Fatal Error: Nesting level too deep - recursive dependency?
294
            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

294
            /** @scrutinizer ignore-type */ range(1, count($array)),
Loading history...
295
296
            SORT_ASC,
297
            SORT_NUMERIC,
298
            $array
299
        );
300
    }
301
}
302