Issues (43)

src/Helper/DbArrayHelper.php (4 issues)

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

316
            /** @scrutinizer ignore-type */ SORT_ASC,
Loading history...
317
            SORT_NUMERIC,
0 ignored issues
show
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

317
            /** @scrutinizer ignore-type */ SORT_NUMERIC,
Loading history...
318
319
            /**
320
             * This fix is used for cases when the main sorting specified by columns has equal values without it will
321
             * lead to Fatal Error: Nesting level too deep - recursive dependency?
322
             */
323
            range(1, count($array)),
0 ignored issues
show
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

323
            /** @scrutinizer ignore-type */ range(1, count($array)),
Loading history...
324
            SORT_ASC,
325
            SORT_NUMERIC,
326
            $array
327
        );
328
    }
329
330
    /**
331
     * Returns the value of an array element or object property with the given path.
332
     *
333
     * This method is internally used to convert the data fetched from a database into the format as required by this
334
     * query.
335
     *
336
     * @param array[] $rows The raw query result from a database.
337
     *
338
     * @return array[]
339
     */
340
    public static function populate(array $rows, Closure|string|null $indexBy = null): array
341
    {
342
        if ($indexBy === null) {
343
            return $rows;
344
        }
345
346
        $result = [];
347
348
        foreach ($rows as $row) {
349
            /** @psalm-suppress MixedArrayOffset */
350
            $result[self::getValueByPath($row, $indexBy)] = $row;
351
        }
352
353
        return $result;
354
    }
355
}
356