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

codeigniter4 / CodeIgniter4 / 8586246081

07 Apr 2024 04:43AM UTC coverage: 86.602% (+1.0%) from 85.607%
8586246081

push

github

web-flow
Merge pull request #8720 from codeigniter4/4.5

Merge 4.5 into develop

2273 of 2603 new or added lines in 188 files covered. (87.32%)

53 existing lines in 18 files now uncovered.

19947 of 23033 relevant lines covered (86.6%)

189.35 hits per line

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

98.02
/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 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,579✔
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,586✔
54
            '/(?<!\\\\)\./',
1,586✔
55
            rtrim($index, '* '),
1,586✔
56
            0,
1,586✔
57
            PREG_SPLIT_NO_EMPTY
1,586✔
58
        );
1,586✔
59

60
        return array_map(
1,586✔
61
            static fn ($key) => str_replace('\.', '.', $key),
1,586✔
62
            $segments
1,586✔
63
        );
1,586✔
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,579✔
77
            return null;
9✔
78
        }
79

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

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

87
        // Handle Wildcard (*)
88
        if ($currentIndex === '*') {
1,358✔
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) => $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,358✔
112
            return $array[$currentIndex];
1,345✔
113
        }
114

115
        // Do we need to recursively search this value?
116
        if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
58✔
117
            return self::arraySearchDot($indexes, $array[$currentIndex]);
55✔
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, '*.*')) {
14✔
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);
12✔
138

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

144
        $currentArray = $array;
12✔
145

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

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

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

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

164
                continue;
1✔
165
            }
166

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

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

174
        return true;
4✔
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 === []) {
6✔
NEW
191
            return $array;
×
192
        }
193

194
        $result = [];
6✔
195

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

200
        return $result;
6✔
201
    }
202

203
    /**
204
     * Recursively attach $row to the $indexes path of values found by
205
     * `dot_array_search()`.
206
     *
207
     * @used-by groupBy()
208
     */
209
    private static function arrayAttachIndexedValue(
210
        array $result,
211
        array $row,
212
        array $indexes,
213
        bool $includeEmpty
214
    ): array {
215
        if (($index = array_shift($indexes)) === null) {
6✔
216
            $result[] = $row;
6✔
217

218
            return $result;
6✔
219
        }
220

221
        $value = dot_array_search($index, $row);
6✔
222

223
        if (! is_scalar($value)) {
6✔
224
            $value = '';
6✔
225
        }
226

227
        if (is_bool($value)) {
6✔
NEW
228
            $value = (int) $value;
×
229
        }
230

231
        if (! $includeEmpty && $value === '') {
6✔
232
            return $result;
3✔
233
        }
234

235
        if (! array_key_exists($value, $result)) {
6✔
236
            $result[$value] = [];
6✔
237
        }
238

239
        $result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty);
6✔
240

241
        return $result;
6✔
242
    }
243

244
    /**
245
     * Compare recursively two associative arrays and return difference as new array.
246
     * Returns keys that exist in `$original` but not in `$compareWith`.
247
     */
248
    public static function recursiveDiff(array $original, array $compareWith): array
249
    {
250
        $difference = [];
12✔
251

252
        if ($original === []) {
12✔
253
            return [];
1✔
254
        }
255

256
        if ($compareWith === []) {
11✔
257
            return $original;
6✔
258
        }
259

260
        foreach ($original as $originalKey => $originalValue) {
5✔
261
            if ($originalValue === []) {
5✔
262
                continue;
4✔
263
            }
264

265
            if (is_array($originalValue)) {
5✔
266
                $diffArrays = [];
5✔
267

268
                if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
5✔
269
                    $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
4✔
270
                } else {
271
                    $difference[$originalKey] = $originalValue;
2✔
272
                }
273

274
                if ($diffArrays !== []) {
5✔
275
                    $difference[$originalKey] = $diffArrays;
1✔
276
                }
277
            } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
4✔
278
                $difference[$originalKey] = $originalValue;
1✔
279
            }
280
        }
281

282
        return $difference;
5✔
283
    }
284

285
    /**
286
     * Recursively count all keys.
287
     */
288
    public static function recursiveCount(array $array, int $counter = 0): int
289
    {
290
        foreach ($array as $value) {
8✔
291
            if (is_array($value)) {
8✔
292
                $counter = self::recursiveCount($value, $counter);
7✔
293
            }
294

295
            $counter++;
8✔
296
        }
297

298
        return $counter;
8✔
299
    }
300

301
    /**
302
     * Sorts array values in natural order
303
     * If the value is an array, you need to specify the $sortByIndex of the key to sort
304
     *
305
     * @param list<int|list<int|string>|string> $array
306
     * @param int|string|null                   $sortByIndex
307
     */
308
    public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool
309
    {
310
        return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex) {
3✔
311
            if ($sortByIndex !== null) {
3✔
312
                return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]);
2✔
313
            }
314

315
            return strnatcmp((string) $currentValue, (string) $nextValue);
1✔
316
        });
3✔
317
    }
318
}
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