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

codeigniter4 / CodeIgniter4 / 37986190232

09 Oct 2026 08:19PM UTC coverage: 50.703% (-39.5%) from 90.183%
37986190232

Pull #10606

github

web-flow
Merge 2cebb1efa into 7822cde17
Pull Request #10606: fix: refresh REQUEST after URI GET normalization

17 of 30 new or added lines in 2 files covered. (56.67%)

10012 existing lines in 288 files now uncovered.

12871 of 25385 relevant lines covered (50.7%)

87.05 hits per line

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

19.8
/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
     * @param array<array-key, mixed> $array
38
     *
39
     * @return mixed
40
     */
41
    public static function dotSearch(string $index, array $array)
42
    {
43
        return self::arraySearchDot(self::convertToArray($index), $array);
99 ✔
44
    }
45

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

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

67
    /**
68
     * Recursively search the array with wildcards.
69
     *
70
     * @used-by dotSearch()
71
     *
72
     * @param list<string>            $indexes
73
     * @param array<array-key, mixed> $array
74
     *
75
     * @return mixed
76
     */
77
    private static function arraySearchDot(array $indexes, array $array)
78
    {
79
        // If index is empty, returns null.
80
        if ($indexes === []) {
99 ✔
UNCOV
81
            return null;
×
82
        }
83

84
        // Grab the current index
85
        $currentIndex = array_shift($indexes);
99 ✔
86

87
        if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
99 ✔
88
            return null;
41 ✔
89
        }
90

91
        // Handle Wildcard (*)
92
        if ($currentIndex === '*') {
76 ✔
UNCOV
93
            $answer = [];
×
94

UNCOV
95
            foreach ($array as $value) {
×
UNCOV
96
                if (! is_array($value)) {
×
UNCOV
97
                    return null;
×
98
                }
99

UNCOV
100
                $answer[] = self::arraySearchDot($indexes, $value);
×
101
            }
102

UNCOV
103
            $answer = array_filter($answer, static fn ($value): bool => $value !== null);
×
104

UNCOV
105
            if ($answer !== []) {
×
106
                // If array only has one element, we return that element for BC.
UNCOV
107
                return count($answer) === 1 ? current($answer) : $answer;
×
108
            }
109

UNCOV
110
            return null;
×
111
        }
112

113
        // If this is the last index, make sure to return it now,
114
        // and not try to recurse through things.
115
        if ($indexes === []) {
76 ✔
116
            return $array[$currentIndex];
76 ✔
117
        }
118

119
        // Do we need to recursively search this value?
120
        if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
5 ✔
121
            return self::arraySearchDot($indexes, $array[$currentIndex]);
5 ✔
122
        }
123

124
        // Otherwise, not found.
UNCOV
125
        return null;
×
126
    }
127

128
    /**
129
     * array_key_exists() with dot array syntax.
130
     *
131
     * If wildcard `*` is used, all items for the key after it must have the key.
132
     *
133
     * @param array<array-key, mixed> $array
134
     */
135
    public static function dotKeyExists(string $index, array $array): bool
136
    {
UNCOV
137
        if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
×
UNCOV
138
            throw new InvalidArgumentException(
×
UNCOV
139
                'You must set key right after "*". Invalid index: "' . $index . '"',
×
UNCOV
140
            );
×
141
        }
142

UNCOV
143
        $indexes = self::convertToArray($index);
×
144

145
        // If indexes is empty, returns false.
UNCOV
146
        if ($indexes === []) {
×
UNCOV
147
            return false;
×
148
        }
149

UNCOV
150
        $currentArray = $array;
×
151

152
        // Grab the current index
UNCOV
153
        while ($currentIndex = array_shift($indexes)) {
×
UNCOV
154
            if ($currentIndex === '*') {
×
UNCOV
155
                $currentIndex = array_shift($indexes);
×
156

UNCOV
157
                foreach ($currentArray as $item) {
×
UNCOV
158
                    if (! array_key_exists($currentIndex, $item)) {
×
UNCOV
159
                        return false;
×
160
                    }
161
                }
162

163
                // If indexes is empty, all elements are checked.
UNCOV
164
                if ($indexes === []) {
×
UNCOV
165
                    return true;
×
166
                }
167

UNCOV
168
                $currentArray = self::dotSearch('*.' . $currentIndex, $currentArray);
×
169

UNCOV
170
                continue;
×
171
            }
172

UNCOV
173
            if (! array_key_exists($currentIndex, $currentArray)) {
×
UNCOV
174
                return false;
×
175
            }
176

UNCOV
177
            $currentArray = $currentArray[$currentIndex];
×
178
        }
179

UNCOV
180
        return true;
×
181
    }
182

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

UNCOV
200
        $result = [];
×
201

UNCOV
202
        foreach ($array as $row) {
×
UNCOV
203
            $result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty);
×
204
        }
205

UNCOV
206
        return $result;
×
207
    }
208

209
    /**
210
     * Recursively attach $row to the $indexes path of values found by
211
     * `dot_array_search()`.
212
     *
213
     * @used-by groupBy()
214
     *
215
     * @param array<array-key, mixed> $result
216
     * @param array<array-key, mixed> $row
217
     * @param list<string>            $indexes
218
     *
219
     * @return array<array-key, mixed>
220
     */
221
    private static function arrayAttachIndexedValue(
222
        array $result,
223
        array $row,
224
        array $indexes,
225
        bool $includeEmpty,
226
    ): array {
UNCOV
227
        if (($index = array_shift($indexes)) === null) {
×
UNCOV
228
            $result[] = $row;
×
229

UNCOV
230
            return $result;
×
231
        }
232

UNCOV
233
        $value = dot_array_search($index, $row);
×
234

UNCOV
235
        if (! is_scalar($value)) {
×
UNCOV
236
            $value = '';
×
237
        }
238

UNCOV
239
        if (is_bool($value)) {
×
240
            $value = (int) $value;
×
241
        }
242

UNCOV
243
        if (! $includeEmpty && $value === '') {
×
UNCOV
244
            return $result;
×
245
        }
246

UNCOV
247
        if (! array_key_exists($value, $result)) {
×
UNCOV
248
            $result[$value] = [];
×
249
        }
250

UNCOV
251
        $result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty);
×
252

UNCOV
253
        return $result;
×
254
    }
255

256
    /**
257
     * Compare recursively two associative arrays and return difference as new array.
258
     * Returns keys that exist in `$original` but not in `$compareWith`.
259
     *
260
     * @param array<array-key, mixed> $original
261
     * @param array<array-key, mixed> $compareWith
262
     *
263
     * @return array<array-key, mixed>
264
     */
265
    public static function recursiveDiff(array $original, array $compareWith): array
266
    {
UNCOV
267
        $difference = [];
×
268

UNCOV
269
        if ($original === []) {
×
UNCOV
270
            return [];
×
271
        }
272

UNCOV
273
        if ($compareWith === []) {
×
UNCOV
274
            return $original;
×
275
        }
276

UNCOV
277
        foreach ($original as $originalKey => $originalValue) {
×
UNCOV
278
            if ($originalValue === []) {
×
UNCOV
279
                continue;
×
280
            }
281

UNCOV
282
            if (is_array($originalValue)) {
×
UNCOV
283
                $diffArrays = [];
×
284

UNCOV
285
                if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
×
UNCOV
286
                    $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
×
287
                } else {
UNCOV
288
                    $difference[$originalKey] = $originalValue;
×
289
                }
290

UNCOV
291
                if ($diffArrays !== []) {
×
UNCOV
292
                    $difference[$originalKey] = $diffArrays;
×
293
                }
UNCOV
294
            } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
×
UNCOV
295
                $difference[$originalKey] = $originalValue;
×
296
            }
297
        }
298

UNCOV
299
        return $difference;
×
300
    }
301

302
    /**
303
     * Recursively count all keys.
304
     *
305
     * @param array<array-key, mixed> $array
306
     */
307
    public static function recursiveCount(array $array, int $counter = 0): int
308
    {
UNCOV
309
        foreach ($array as $value) {
×
UNCOV
310
            if (is_array($value)) {
×
UNCOV
311
                $counter = self::recursiveCount($value, $counter);
×
312
            }
313

UNCOV
314
            $counter++;
×
315
        }
316

UNCOV
317
        return $counter;
×
318
    }
319

320
    /**
321
     * Sorts array values in natural order
322
     * If the value is an array, you need to specify the $sortByIndex of the key to sort
323
     *
324
     * @param list<int|list<int|string>|string> $array
325
     * @param int|string|null                   $sortByIndex
326
     */
327
    public static function sortValuesByNatural(array &$array, $sortByIndex = null): true
328
    {
UNCOV
329
        return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex): int {
×
UNCOV
330
            if ($sortByIndex !== null) {
×
UNCOV
331
                return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]);
×
332
            }
333

UNCOV
334
            return strnatcmp((string) $currentValue, (string) $nextValue);
×
UNCOV
335
        });
×
336
    }
337
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc