Passed
Pull Request — master (#540)
by Def
02:57
created

ArrayHelper::populate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

311
            /** @scrutinizer ignore-type */ SORT_NUMERIC,
Loading history...
312
313
            /**
314
             * This fix is used for cases when main sorting specified by columns has equal values without it will lead
315
             * to Fatal Error: Nesting level too deep - recursive dependency?
316
             */
317
            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

317
            /** @scrutinizer ignore-type */ range(1, count($array)),
Loading history...
318
            SORT_ASC,
319
            SORT_NUMERIC,
320
            $array
321
        );
322
    }
323
324
    /**
325
     * @todo - Write correct description
326
     * Converts the array with data, using index-key if needed
327
     *
328
     * This method is internally used to convert the data fetched from database into the format as required by this
329
     * query.
330
     *
331
     * @param array $rows the raw query result from database.
332
     * @psalm-suppress MixedArrayOffset
333
     *
334
     * @return array the converted query result.
335
     */
336
    public static function populate(array $rows, Closure|string|null $indexBy = null): array
337
    {
338
        if ($indexBy === null) {
339
            return $rows;
340
        }
341
342
        $result = [];
343
344
        /** @psalm-var array[][] $row */
345
        foreach ($rows as $row) {
346
            $result[self::getValueByPath($row, $indexBy)] = $row;
347
        }
348
349
        return $result;
350
    }
351
}
352