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

codeigniter4 / CodeIgniter4 / 7138325381

08 Dec 2023 06:49AM UTC coverage: 85.015% (+0.03%) from 84.987%
7138325381

push

github

web-flow
Merge pull request #8308 from kenjis/fix-ArrayHelper-sortValuesByNatural

refactor: TypeError in strict mode by ArrayHelper::SortValuesByNatural()

2 of 2 new or added lines in 1 file covered. (100.0%)

6 existing lines in 2 files now uncovered.

19261 of 22656 relevant lines covered (85.02%)

193.82 hits per line

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

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

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

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

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

87
        // Handle Wildcard (*)
88
        if ($currentIndex === '*') {
1,356✔
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 (count($answer) === 1) {
8✔
103
                    // If array only has one element, we return that element for BC.
104
                    return current($answer);
6✔
105
                }
106

107
                return $answer;
3✔
108
            }
109

110
            return null;
1✔
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 === []) {
1,356✔
116
            return $array[$currentIndex];
1,343✔
117
        }
118

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

124
        // Otherwise, not found.
125
        return null;
6✔
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
    public static function dotKeyExists(string $index, array $array): bool
134
    {
135
        if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
14✔
136
            throw new InvalidArgumentException(
2✔
137
                'You must set key right after "*". Invalid index: "' . $index . '"'
2✔
138
            );
2✔
139
        }
140

141
        $indexes = self::convertToArray($index);
12✔
142

143
        // If indexes is empty, returns false.
144
        if ($indexes === []) {
12✔
145
            return false;
1✔
146
        }
147

148
        $currentArray = $array;
12✔
149

150
        // Grab the current index
151
        while ($currentIndex = array_shift($indexes)) {
12✔
152
            if ($currentIndex === '*') {
12✔
153
                $currentIndex = array_shift($indexes);
7✔
154

155
                foreach ($currentArray as $item) {
7✔
156
                    if (! array_key_exists($currentIndex, $item)) {
7✔
157
                        return false;
5✔
158
                    }
159
                }
160

161
                // If indexes is empty, all elements are checked.
162
                if ($indexes === []) {
3✔
163
                    return true;
3✔
164
                }
165

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

168
                continue;
1✔
169
            }
170

171
            if (! array_key_exists($currentIndex, $currentArray)) {
12✔
172
                return false;
4✔
173
            }
174

175
            $currentArray = $currentArray[$currentIndex];
10✔
176
        }
177

178
        return true;
4✔
179
    }
180

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

198
        $result = [];
6✔
199

200
        foreach ($array as $row) {
6✔
201
            $result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty);
6✔
202
        }
203

204
        return $result;
6✔
205
    }
206

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

222
            return $result;
6✔
223
        }
224

225
        $value = dot_array_search($index, $row);
6✔
226

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

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

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

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

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

245
        return $result;
6✔
246
    }
247

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

256
        if ($original === []) {
12✔
257
            return [];
1✔
258
        }
259

260
        if ($compareWith === []) {
11✔
261
            return $original;
6✔
262
        }
263

264
        foreach ($original as $originalKey => $originalValue) {
5✔
265
            if ($originalValue === []) {
5✔
266
                continue;
4✔
267
            }
268

269
            if (is_array($originalValue)) {
5✔
270
                $diffArrays = [];
5✔
271

272
                if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
5✔
273
                    $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
4✔
274
                } else {
275
                    $difference[$originalKey] = $originalValue;
2✔
276
                }
277

278
                if ($diffArrays !== []) {
5✔
279
                    $difference[$originalKey] = $diffArrays;
5✔
280
                }
281
            } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
4✔
282
                $difference[$originalKey] = $originalValue;
1✔
283
            }
284
        }
285

286
        return $difference;
5✔
287
    }
288

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

299
            $counter++;
8✔
300
        }
301

302
        return $counter;
8✔
303
    }
304

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

319
            return strnatcmp((string) $currentValue, (string) $nextValue);
1✔
320
        });
3✔
321
    }
322
}
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