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

codeigniter4 / CodeIgniter4 / 28811663512

06 Jul 2026 05:48PM UTC coverage: 89.203% (+0.04%) from 89.168%
28811663512

Pull #10363

github

web-flow
Merge d373bc767 into b36509019
Pull Request #10363: feat: Add ID-based model chunking

44 of 45 new or added lines in 2 files covered. (97.78%)

1 existing line in 1 file now uncovered.

25158 of 28203 relevant lines covered (89.2%)

228.02 hits per line

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

94.04
/system/Config/BaseConfig.php
1
<?php
2

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

12
namespace CodeIgniter\Config;
13

14
use CodeIgniter\Autoloader\FileLocatorInterface;
15
use CodeIgniter\Exceptions\ConfigException;
16
use CodeIgniter\Exceptions\RuntimeException;
17
use Config\Encryption;
18
use Config\Modules;
19
use ReflectionClass;
20
use ReflectionException;
21

22
/**
23
 * Class BaseConfig
24
 *
25
 * Not intended to be used on its own, this class will attempt to
26
 * automatically populate the child class' properties with values
27
 * from the environment.
28
 *
29
 * These can be set within the .env file.
30
 *
31
 * @phpstan-consistent-constructor
32
 * @see \CodeIgniter\Config\BaseConfigTest
33
 */
34
class BaseConfig
35
{
36
    /**
37
     * An optional array of classes that will act as Registrars
38
     * for rapidly setting config class properties.
39
     *
40
     * @var array
41
     */
42
    public static $registrars = [];
43

44
    /**
45
     * Whether to override properties by Env vars and Registrars.
46
     */
47
    public static bool $override = true;
48

49
    /**
50
     * Has module discovery completed?
51
     *
52
     * @var bool
53
     */
54
    protected static $didDiscovery = false;
55

56
    /**
57
     * Is module discovery running or not?
58
     */
59
    protected static bool $discovering = false;
60

61
    /**
62
     * The processing Registrar file for error message.
63
     */
64
    protected static string $registrarFile = '';
65

66
    /**
67
     * The modules configuration.
68
     *
69
     * @var Modules|null
70
     */
71
    protected static $moduleConfig;
72

73
    public static function __set_state(array $array)
74
    {
75
        static::$override = false;
2✔
76
        $obj              = new static();
2✔
77
        static::$override = true;
2✔
78

79
        $properties = array_keys(get_object_vars($obj));
2✔
80

81
        foreach ($properties as $property) {
2✔
82
            $obj->{$property} = $array[$property];
2✔
83
        }
84

85
        return $obj;
2✔
86
    }
87

88
    /**
89
     * @internal For testing purposes only.
90
     * @testTag
91
     */
92
    public static function setModules(Modules $modules): void
93
    {
94
        static::$moduleConfig = $modules;
1✔
95
    }
96

97
    /**
98
     * @internal For testing purposes only.
99
     * @testTag
100
     */
101
    public static function reset(): void
102
    {
103
        static::$registrars   = [];
23✔
104
        static::$override     = true;
23✔
105
        static::$didDiscovery = false;
23✔
106
        static::$moduleConfig = null;
23✔
107
    }
108

109
    /**
110
     * Will attempt to get environment variables with names
111
     * that match the properties of the child class.
112
     *
113
     * The "shortPrefix" is the lowercase-only config class name.
114
     */
115
    public function __construct()
116
    {
117
        static::$moduleConfig ??= new Modules();
8,354✔
118

119
        if (! static::$override) {
8,354✔
120
            return;
6✔
121
        }
122

123
        $this->registerProperties();
8,350✔
124

125
        $properties  = array_keys(get_object_vars($this));
8,350✔
126
        $prefix      = static::class;
8,350✔
127
        $slashAt     = strrpos($prefix, '\\');
8,350✔
128
        $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1));
8,350✔
129

130
        foreach ($properties as $property) {
8,350✔
131
            $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
8,350✔
132

133
            if ($this instanceof Encryption) {
8,350✔
134
                if ($property === 'key') {
50✔
135
                    $this->{$property} = $this->parseEncryptionKey($this->{$property});
50✔
136
                } elseif ($property === 'previousKeys') {
50✔
137
                    $keysArray  = is_string($this->{$property}) ? array_map(trim(...), explode(',', $this->{$property})) : $this->{$property};
50✔
138
                    $parsedKeys = [];
50✔
139

140
                    foreach ($keysArray as $key) {
50✔
141
                        $parsedKeys[] = $this->parseEncryptionKey($key);
50✔
142
                    }
143

144
                    $this->{$property} = $parsedKeys;
50✔
145
                }
146
            }
147
        }
148
    }
149

150
    /**
151
     * Parse encryption key with hex2bin: or base64: prefix
152
     */
153
    protected function parseEncryptionKey(string $key): string
154
    {
155
        if (str_starts_with($key, 'hex2bin:')) {
50✔
156
            return hex2bin(substr($key, 8));
2✔
157
        }
158

159
        if (str_starts_with($key, 'base64:')) {
50✔
160
            return base64_decode(substr($key, 7), true);
2✔
161
        }
162

163
        return $key;
50✔
164
    }
165

166
    /**
167
     * Initialization an environment-specific configuration setting
168
     *
169
     * @param array<int|string, mixed>|bool|float|int|string|null $property
170
     *
171
     * @return void
172
     */
173
    protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix)
174
    {
175
        if (is_array($property)) {
8,350✔
176
            foreach (array_keys($property) as $key) {
8,333✔
177
                $this->initEnvValue($property[$key], "{$name}.{$key}", $prefix, $shortPrefix);
8,333✔
178
            }
179
        } elseif (($value = $this->getEnvValue($name, $prefix, $shortPrefix)) !== false && $value !== null) {
8,350✔
180
            if ($value === 'false') {
8,266✔
181
                $value = false;
10✔
182
            } elseif ($value === 'true') {
8,266✔
183
                $value = true;
10✔
184
            }
185
            if (is_bool($value)) {
8,266✔
186
                $property = $value;
10✔
187

188
                return;
10✔
189
            }
190

191
            $value = trim($value, '\'"');
8,266✔
192

193
            if (is_int($property)) {
8,266✔
194
                $value = (int) $value;
10✔
195
            } elseif (is_float($property)) {
8,266✔
196
                $value = (float) $value;
10✔
197
            }
198

199
            // If the default value of the property is `null` and the type is not
200
            // `string`, TypeError will happen.
201
            // So cannot set `declare(strict_types=1)` in this file.
202
            $property = $value;
8,266✔
203
        }
204
    }
205

206
    /**
207
     * Retrieve an environment-specific configuration setting
208
     *
209
     * @return string|null
210
     */
211
    protected function getEnvValue(string $property, string $prefix, string $shortPrefix)
212
    {
213
        $shortPrefix        = ltrim($shortPrefix, '\\');
8,350✔
214
        $underscoreProperty = str_replace('.', '_', $property);
8,350✔
215

216
        switch (true) {
217
            case array_key_exists("{$shortPrefix}.{$property}", $_ENV):
8,350✔
218
                return $_ENV["{$shortPrefix}.{$property}"];
12✔
219

220
            case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_ENV):
8,350✔
221
                return $_ENV["{$shortPrefix}_{$underscoreProperty}"];
10✔
222

223
            case array_key_exists("{$shortPrefix}.{$property}", $_SERVER):
8,350✔
224
                return $_SERVER["{$shortPrefix}.{$property}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
8,266✔
225

226
            case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_SERVER):
8,350✔
227
                return $_SERVER["{$shortPrefix}_{$underscoreProperty}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
×
228

229
            case array_key_exists("{$prefix}.{$property}", $_ENV):
8,350✔
230
                return $_ENV["{$prefix}.{$property}"];
10✔
231

232
            case array_key_exists("{$prefix}_{$underscoreProperty}", $_ENV):
8,350✔
233
                return $_ENV["{$prefix}_{$underscoreProperty}"];
10✔
234

235
            case array_key_exists("{$prefix}.{$property}", $_SERVER):
8,350✔
236
                return $_SERVER["{$prefix}.{$property}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
1✔
237

238
            case array_key_exists("{$prefix}_{$underscoreProperty}", $_SERVER):
8,350✔
239
                return $_SERVER["{$prefix}_{$underscoreProperty}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service)
×
240

241
            default:
242
                $value = getenv("{$shortPrefix}.{$property}");
8,350✔
243
                $value = $value === false ? getenv("{$shortPrefix}_{$underscoreProperty}") : $value;
8,350✔
244
                $value = $value === false ? getenv("{$prefix}.{$property}") : $value;
8,350✔
245
                $value = $value === false ? getenv("{$prefix}_{$underscoreProperty}") : $value;
8,350✔
246

247
                return $value === false ? null : $value;
8,350✔
248
        }
249
    }
250

251
    /**
252
     * Provides external libraries a simple way to register one or more
253
     * options into a config file.
254
     *
255
     * @return void
256
     *
257
     * @throws ReflectionException
258
     */
259
    protected function registerProperties()
260
    {
261
        if (! static::$moduleConfig->shouldDiscover('registrars')) {
8,350✔
262
            return;
1✔
263
        }
264

265
        if (! static::$didDiscovery) {
8,350✔
266
            // Discovery must be completed before the first instantiation of any Config class.
267
            if (static::$discovering) {
24✔
268
                throw new ConfigException(
×
269
                    'During Auto-Discovery of Registrars,'
×
270
                    . ' "' . static::class . '" executes Auto-Discovery again.'
×
271
                    . ' "' . clean_path(static::$registrarFile) . '" seems to have bad code.',
×
272
                );
×
273
            }
274

275
            static::$discovering = true;
24✔
276

277
            /** @var FileLocatorInterface */
278
            $locator         = service('locator');
24✔
279
            $registrarsFiles = $locator->search('Config/Registrar.php');
24✔
280

281
            foreach ($registrarsFiles as $file) {
24✔
282
                // Saves the file for error message.
283
                static::$registrarFile = $file;
24✔
284

285
                $className = $locator->findQualifiedNameFromPath($file);
24✔
286

287
                if ($className === false) {
24✔
288
                    continue;
×
289
                }
290

291
                static::$registrars[] = new $className();
24✔
292
            }
293

294
            static::$didDiscovery = true;
24✔
295
            static::$discovering  = false;
24✔
296
        }
297

298
        $shortName = (new ReflectionClass($this))->getShortName();
8,350✔
299

300
        // Check the registrar class for a method named after this class' shortName
301
        foreach (static::$registrars as $callable) {
8,350✔
302
            // ignore non-applicable registrars
303
            if (! method_exists($callable, $shortName)) {
8,350✔
304
                continue; // @codeCoverageIgnore
305
            }
306

307
            $properties = $callable::$shortName();
1,071✔
308

309
            if (! is_array($properties)) {
1,071✔
310
                throw new RuntimeException('Registrars must return an array of properties and their values.');
1✔
311
            }
312

313
            foreach ($properties as $property => $value) {
1,070✔
314
                // Directives are recognized only at the property root.
315
                if ($value instanceof Merge) {
1,070✔
316
                    $this->{$property} = $this->applyMerge($this->{$property} ?? null, $value);
4✔
317

318
                    continue;
4✔
319
                }
320

321
                // Legacy behavior - unchanged, and on the hot path with no extra checks.
322
                if (isset($this->{$property}) && is_array($this->{$property}) && is_array($value)) {
1,066✔
323
                    $this->{$property} = array_merge($this->{$property}, $value);
1,066✔
324
                } else {
UNCOV
325
                    $this->{$property} = $value;
×
326
                }
327
            }
328
        }
329
    }
330

331
    /**
332
     * Applies a property-root Merge directive against the current value.
333
     *
334
     * REPLACE is terminal - its payload is taken verbatim. The list strategies
335
     * (APPEND/PREPEND/BEFORE/AFTER) resolve via mergeList(). BY_KEY recurses via
336
     * mergeByKey(), honoring nested directives.
337
     */
338
    private function applyMerge(mixed $current, Merge $directive): mixed
339
    {
340
        return match ($directive->strategy) {
39✔
341
            Merge::REPLACE                                             => $directive->value,
39✔
342
            Merge::BY_KEY                                              => $this->mergeByKey(is_array($current) ? $current : [], $directive->value),
34✔
343
            Merge::APPEND, Merge::PREPEND, Merge::BEFORE, Merge::AFTER => $this->mergeList(is_array($current) ? $current : [], $directive),
39✔
344
        };
39✔
345
    }
346

347
    /**
348
     * Resolves a list directive (APPEND, PREPEND, BEFORE, AFTER) against the
349
     * current value treated as a list.
350
     *
351
     * The directives never introduce a duplicate value: the incoming payload is
352
     * de-duplicated against itself (keeping first-seen order) and values already
353
     * in the list are not added again. Duplicates that already exist in the
354
     * current list are left untouched. Then:
355
     *  - APPEND/PREPEND add only the values that are absent - already-present
356
     *    values are left where they are (no relocation).
357
     *  - BEFORE/AFTER move an already-present value to the anchor position, but
358
     *    only when the anchor exists. If the anchor is missing they fall back to
359
     *    APPEND/PREPEND respectively and do not relocate already-present values.
360
     *
361
     * The anchor is matched strictly (===) against the list elements, using the
362
     * first match. Do not use a value as both the anchor and an inserted value.
363
     *
364
     * @param array<array-key, mixed> $current
365
     *
366
     * @return list<mixed>
367
     */
368
    private function mergeList(array $current, Merge $directive): array
369
    {
370
        $current = array_values($current);
29✔
371

372
        // De-duplicate the payload itself (strict, first-seen order) so a value
373
        // repeated within it is not inserted twice.
374
        $incoming = [];
29✔
375

376
        foreach ($directive->value as $value) {
29✔
377
            if (! in_array($value, $incoming, true)) {
29✔
378
                $incoming[] = $value;
29✔
379
            }
380
        }
381

382
        $anchored    = $directive->strategy === Merge::BEFORE || $directive->strategy === Merge::AFTER;
29✔
383
        $anchorFound = $anchored && in_array($directive->anchor, $current, true);
29✔
384

385
        if ($anchorFound) {
29✔
386
            // Move-to-position: pull out any present copies, then insert the
387
            // whole incoming block at the (recomputed) anchor position.
388
            $current = array_values(array_filter(
11✔
389
                $current,
11✔
390
                static fn ($value): bool => ! in_array($value, $incoming, true),
11✔
391
            ));
11✔
392

393
            $index  = (int) array_search($directive->anchor, $current, true);
11✔
394
            $offset = $directive->strategy === Merge::AFTER ? $index + 1 : $index;
11✔
395

396
            array_splice($current, $offset, 0, $incoming);
11✔
397

398
            return $current;
11✔
399
        }
400

401
        // APPEND/PREPEND, or BEFORE/AFTER with a missing anchor: add only the
402
        // values not already present, without relocating anything.
403
        $incoming = array_values(array_filter(
20✔
404
            $incoming,
20✔
405
            static fn ($value): bool => ! in_array($value, $current, true),
20✔
406
        ));
20✔
407

408
        return $directive->strategy === Merge::PREPEND || $directive->strategy === Merge::BEFORE
20✔
409
            ? array_merge($incoming, $current)
6✔
410
            : array_merge($current, $incoming);
20✔
411
    }
412

413
    /**
414
     * Recursive by-key merge used by Merge::byKey(): string keys recurse, integer
415
     * keys append, scalar leaves are replaced, and nested Merge directives are
416
     * honored. A missing/non-array current child uses [] as its base, so directives
417
     * in brand-new subtrees are still resolved.
418
     *
419
     * @param array<array-key, mixed> $current
420
     * @param array<array-key, mixed> $incoming
421
     *
422
     * @return array<array-key, mixed>
423
     */
424
    private function mergeByKey(array $current, array $incoming): array
425
    {
426
        foreach ($incoming as $key => $value) {
12✔
427
            if ($value instanceof Merge) {
12✔
428
                if (is_int($key)) {
8✔
429
                    // No stable current element at an appended position; resolve against null.
430
                    $current[] = $this->applyMerge(null, $value);
1✔
431
                } else {
432
                    $current[$key] = $this->applyMerge($current[$key] ?? null, $value);
7✔
433
                }
434
            } elseif (is_int($key)) {
7✔
435
                $current[] = $value;
4✔
436
            } elseif (is_array($value)) {
7✔
437
                $current[$key] = $this->mergeByKey(
6✔
438
                    isset($current[$key]) && is_array($current[$key]) ? $current[$key] : [],
6✔
439
                    $value,
6✔
440
                );
6✔
441
            } else {
442
                $current[$key] = $value;
4✔
443
            }
444
        }
445

446
        return $current;
12✔
447
    }
448
}
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