• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 26229966610

21 May 2026 01:45PM UTC coverage: 88.169% (-0.03%) from 88.2%
26229966610

Pull #10226

github

web-flow
Merge da3a0d9da into 64787583d
Pull Request #10226: fix: allow array_group_by to group object rows

17 of 28 new or added lines in 1 file covered. (60.71%)

13 existing lines in 1 file now uncovered.

22140 of 25111 relevant lines covered (88.17%)

210.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.84
/system/Helpers/Array/ArrayHelper.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Helpers\Array;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17

18
/**
19
 * @interal This is internal implementation for the framework.
20
 *
21
 * If there are any methods that should be provided, make them
22
 * public APIs via helper functions.
23
 *
24
 * @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest
25
 * @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest
26
 * @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest
27
 */
28
final class ArrayHelper
29
{
30
    /**
31
     * Searches an array through dot syntax. Supports wildcard searches,
32
     * like `foo.*.bar`.
33
     *
34
     * @used-by dot_array_search()
35
     *
36
     * @param string $index The index as dot array syntax.
37
     *
38
     * @return array|bool|int|object|string|null
39
     */
40
    public static function dotSearch(string $index, array $array)
41
    {
42
        return self::arraySearchDot(self::convertToArray($index), $array);
1,619✔
43
    }
44

45
    /**
46
     * @param string $index The index as dot array syntax.
47
     *
48
     * @return list<string> The index as an array.
49
     */
50
    private static function convertToArray(string $index): array
51
    {
52
        // See https://regex101.com/r/44Ipql/1
53
        $segments = preg_split(
1,638✔
54
            '/(?<!\\\\)\./',
1,638✔
55
            rtrim($index, '* '),
1,638✔
56
            0,
1,638✔
57
            PREG_SPLIT_NO_EMPTY,
1,638✔
58
        );
1,638✔
59

60
        return array_map(
1,638✔
61
            static fn ($key): string => str_replace('\.', '.', $key),
1,638✔
62
            $segments,
1,638✔
63
        );
1,638✔
64
    }
65

66
    /**
67
     * Recursively search the array with wildcards.
68
     *
69
     * @used-by dotSearch()
70
     *
71
     * @return array|bool|float|int|object|string|null
72
     */
73
    private static function arraySearchDot(array $indexes, array $array)
74
    {
75
        // If index is empty, returns null.
76
        if ($indexes === []) {
1,619✔
77
            return null;
9✔
78
        }
79

80
        // Grab the current index
81
        $currentIndex = array_shift($indexes);
1,610✔
82

83
        if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
1,610✔
84
            return null;
260✔
85
        }
86

87
        // Handle Wildcard (*)
88
        if ($currentIndex === '*') {
1,388✔
89
            $answer = [];
10✔
90

91
            foreach ($array as $value) {
10✔
92
                if (! is_array($value)) {
10✔
93
                    return null;
1✔
94
                }
95

96
                $answer[] = self::arraySearchDot($indexes, $value);
9✔
97
            }
98

99
            $answer = array_filter($answer, static fn ($value): bool => $value !== null);
9✔
100

101
            if ($answer !== []) {
9✔
102
                // If array only has one element, we return that element for BC.
103
                return count($answer) === 1 ? current($answer) : $answer;
8✔
104
            }
105

106
            return null;
1✔
107
        }
108

109
        // If this is the last index, make sure to return it now,
110
        // and not try to recurse through things.
111
        if ($indexes === []) {
1,388✔
112
            return $array[$currentIndex];
1,375✔
113
        }
114

115
        // Do we need to recursively search this value?
116
        if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
56✔
117
            return self::arraySearchDot($indexes, $array[$currentIndex]);
53✔
118
        }
119

120
        // Otherwise, not found.
121
        return null;
6✔
122
    }
123

124
    /**
125
     * array_key_exists() with dot array syntax.
126
     *
127
     * If wildcard `*` is used, all items for the key after it must have the key.
128
     */
129
    public static function dotKeyExists(string $index, array $array): bool
130
    {
131
        if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
22✔
132
            throw new InvalidArgumentException(
2✔
133
                'You must set key right after "*". Invalid index: "' . $index . '"',
2✔
134
            );
2✔
135
        }
136

137
        $indexes = self::convertToArray($index);
20✔
138

139
        // If indexes is empty, returns false.
140
        if ($indexes === []) {
20✔
141
            return false;
1✔
142
        }
143

144
        $currentArray = $array;
20✔
145

146
        // Grab the current index
147
        while ($currentIndex = array_shift($indexes)) {
20✔
148
            if ($currentIndex === '*') {
20✔
149
                $currentIndex = array_shift($indexes);
11✔
150

151
                foreach ($currentArray as $item) {
11✔
152
                    if (! array_key_exists($currentIndex, $item)) {
11✔
153
                        return false;
7✔
154
                    }
155
                }
156

157
                // If indexes is empty, all elements are checked.
158
                if ($indexes === []) {
5✔
159
                    return true;
5✔
160
                }
161

162
                $currentArray = self::dotSearch('*.' . $currentIndex, $currentArray);
1✔
163

164
                continue;
1✔
165
            }
166

167
            if (! array_key_exists($currentIndex, $currentArray)) {
20✔
168
                return false;
4✔
169
            }
170

171
            $currentArray = $currentArray[$currentIndex];
18✔
172
        }
173

174
        return true;
8✔
175
    }
176

177
    /**
178
     * Groups all rows by their index values. Result's depth equals number of indexes
179
     *
180
     * @used-by array_group_by()
181
     *
182
     * @param array $array        Data array (i.e. from query result)
183
     * @param array $indexes      Indexes to group by. Dot syntax used. Returns $array if empty
184
     * @param bool  $includeEmpty If true, null and '' are also added as valid keys to group
185
     *
186
     * @return array Result array where rows are grouped together by indexes values.
187
     */
188
    public static function groupBy(array $array, array $indexes, bool $includeEmpty = false): array
189
    {
190
        if ($indexes === []) {
8✔
UNCOV
191
            return $array;
×
192
        }
193

194
        $result = [];
8✔
195

196
        foreach ($array as $row) {
8✔
197
            $result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty);
8✔
198
        }
199

200
        return $result;
8✔
201
    }
202

203
    /**
204
     * Recursively attach $row to the $indexes path of values found by
205
     * dot syntax.
206
     *
207
     * @used-by groupBy()
208
     *
209
     * @param array<array-key, mixed>|object $row
210
     * @param list<string>                   $indexes
211
     */
212
    private static function arrayAttachIndexedValue(
213
        array $result,
214
        array|object $row,
215
        array $indexes,
216
        bool $includeEmpty,
217
    ): array {
218
        if (($index = array_shift($indexes)) === null) {
8✔
219
            $result[] = $row;
8✔
220

221
            return $result;
8✔
222
        }
223

224
        $value = self::arrayObjectSearchDot(self::convertToArray($index), $row);
8✔
225

226
        if (! is_scalar($value)) {
8✔
227
            $value = '';
6✔
228
        }
229

230
        if (is_bool($value)) {
8✔
UNCOV
231
            $value = (int) $value;
×
232
        }
233

234
        if (! $includeEmpty && $value === '') {
8✔
235
            return $result;
3✔
236
        }
237

238
        if (! array_key_exists($value, $result)) {
8✔
239
            $result[$value] = [];
8✔
240
        }
241

242
        $result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty);
8✔
243

244
        return $result;
8✔
245
    }
246

247
    /**
248
     * Recursively search an array or object with wildcards.
249
     *
250
     * @param list<string>                   $indexes
251
     * @param array<array-key, mixed>|object $data
252
     *
253
     * @return array<array-key, mixed>|bool|float|int|object|string|null
254
     */
255
    private static function arrayObjectSearchDot(array $indexes, array|object $data)
256
    {
257
        // If index is empty, returns null.
258
        if ($indexes === []) {
8✔
NEW
UNCOV
259
            return null;
×
260
        }
261

262
        // Grab the current index
263
        $currentIndex = array_shift($indexes);
8✔
264

265
        if ($currentIndex === '*') {
8✔
NEW
UNCOV
266
            $answer = [];
×
267

NEW
UNCOV
268
            foreach ($data as $value) {
×
NEW
UNCOV
269
                if (! is_array($value) && ! is_object($value)) {
×
NEW
UNCOV
270
                    return null;
×
271
                }
272

NEW
UNCOV
273
                $answer[] = self::arrayObjectSearchDot($indexes, $value);
×
274
            }
275

NEW
UNCOV
276
            $answer = array_filter($answer, static fn ($value): bool => $value !== null);
×
277

NEW
UNCOV
278
            if ($answer !== []) {
×
279
                // If array only has one element, we return that element for BC.
NEW
UNCOV
280
                return count($answer) === 1 ? current($answer) : $answer;
×
281
            }
282

NEW
UNCOV
283
            return null;
×
284
        }
285

286
        if (! self::arrayObjectKeyExists($currentIndex, $data)) {
8✔
287
            return null;
6✔
288
        }
289

290
        $value = self::arrayObjectValue($currentIndex, $data);
8✔
291

292
        // If this is the last index, make sure to return it now,
293
        // and not try to recurse through things.
294
        if ($indexes === []) {
8✔
295
            return $value;
8✔
296
        }
297

298
        // Do we need to recursively search this value?
299
        if ((is_array($value) && $value !== []) || is_object($value)) {
3✔
300
            return self::arrayObjectSearchDot($indexes, $value);
3✔
301
        }
302

303
        // Otherwise, not found.
NEW
UNCOV
304
        return null;
×
305
    }
306

307
    /**
308
     * @param array<array-key, mixed>|object $data
309
     */
310
    private static function arrayObjectKeyExists(string $key, array|object $data): bool
311
    {
312
        if (is_array($data)) {
8✔
313
            return isset($data[$key]);
6✔
314
        }
315

316
        return array_key_exists($key, get_object_vars($data));
2✔
317
    }
318

319
    /**
320
     * @param array<array-key, mixed>|object $data
321
     */
322
    private static function arrayObjectValue(string $key, array|object $data): mixed
323
    {
324
        if (is_array($data)) {
8✔
325
            return $data[$key];
6✔
326
        }
327

328
        return get_object_vars($data)[$key];
2✔
329
    }
330

331
    /**
332
     * Compare recursively two associative arrays and return difference as new array.
333
     * Returns keys that exist in `$original` but not in `$compareWith`.
334
     */
335
    public static function recursiveDiff(array $original, array $compareWith): array
336
    {
337
        $difference = [];
12✔
338

339
        if ($original === []) {
12✔
340
            return [];
1✔
341
        }
342

343
        if ($compareWith === []) {
11✔
344
            return $original;
6✔
345
        }
346

347
        foreach ($original as $originalKey => $originalValue) {
5✔
348
            if ($originalValue === []) {
5✔
349
                continue;
4✔
350
            }
351

352
            if (is_array($originalValue)) {
5✔
353
                $diffArrays = [];
5✔
354

355
                if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
5✔
356
                    $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
4✔
357
                } else {
358
                    $difference[$originalKey] = $originalValue;
2✔
359
                }
360

361
                if ($diffArrays !== []) {
5✔
362
                    $difference[$originalKey] = $diffArrays;
1✔
363
                }
364
            } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
4✔
365
                $difference[$originalKey] = $originalValue;
1✔
366
            }
367
        }
368

369
        return $difference;
5✔
370
    }
371

372
    /**
373
     * Recursively count all keys.
374
     */
375
    public static function recursiveCount(array $array, int $counter = 0): int
376
    {
377
        foreach ($array as $value) {
8✔
378
            if (is_array($value)) {
8✔
379
                $counter = self::recursiveCount($value, $counter);
7✔
380
            }
381

382
            $counter++;
8✔
383
        }
384

385
        return $counter;
8✔
386
    }
387

388
    /**
389
     * Sorts array values in natural order
390
     * If the value is an array, you need to specify the $sortByIndex of the key to sort
391
     *
392
     * @param list<int|list<int|string>|string> $array
393
     * @param int|string|null                   $sortByIndex
394
     */
395
    public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool
396
    {
397
        return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex): int {
3✔
398
            if ($sortByIndex !== null) {
3✔
399
                return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]);
2✔
400
            }
401

402
            return strnatcmp((string) $currentValue, (string) $nextValue);
1✔
403
        });
3✔
404
    }
405
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc