Repository navigation
perf: scan the environment once when building config classes - #10657
lonnieezell wants to merge 4 commits into
Conversation
…fig classes `BaseConfig::__construct()` checked every leaf property against `$_ENV`, `$_SERVER` and four `getenv()` calls, so the common no-override path was also the slowest. The environment is now indexed once, grouped by the text before the first "." or "_", and each class only walks the keys that match its prefixes. The index is rebuilt when `$_ENV` or `$_SERVER` changes (once per request in Worker Mode) or on `BaseConfig::reset()`. PHP's `getenv()` array omits names containing ".", " " or "[", so those are looked up with `getenv()` on each class's first build and cached for the process. Classes that override `getEnvValue()` or `initEnvValue()` keep the previous live lookups. Lookup precedence is unchanged. Building 20 core config classes (PHP 8.3 CLI, no opcache, median): | Scenario | Before | After | |---------------------------------------|----------|----------| | Warm, no overrides, new $_SERVER | 0.456 ms | 0.073 ms | | Warm, no overrides, unchanged env | 0.456 ms | 0.031 ms | | Warm, 5 overrides, new $_SERVER | 0.467 ms | 0.110 ms | | Warm, 5 overrides, unchanged env | 0.471 ms | 0.067 ms | | Cold first build, no overrides | 0.493 ms | 0.205 ms | | Cold first build, 5 overrides | 0.505 ms | 0.267 ms | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The per-class cache can silently miss existing OS-only overrides for array leaves introduced by later instances.
1 open issue
What changed in this PR
Optimizes config construction for the 4.8 release by reusing an environment index, addressing #10615’s repeated lookup costs without config caching and in Worker Mode.
Changes:
- Adds indexed environment lookups, snapshot-based refresh, and live-lookup fallback for custom hooks.
- Adds regression coverage for precedence, refresh behavior, array keys, and unusual environment names.
- Documents changes to late
putenv()updates and Windows case sensitivity.
Validation was limited to targeted source inspection; runtime tests remain for CI.
| File | Description |
|---|---|
user_guide_src/source/installation/upgrade_480.rst |
Explains environment lookup behavior changes. |
user_guide_src/source/changelogs/v4.8.0.rst |
Records the optimization and compatibility notes. |
tests/system/Config/fixtures/Snake_Config.php |
Provides an underscore-containing class name fixture. |
tests/system/Config/fixtures/EnvIndexConfig.php |
Provides scalar and array override fixtures. |
tests/system/Config/fixtures/CustomEnvHookConfig.php |
Provides a custom environment hook fixture. |
tests/system/Config/BaseConfigTest.php |
Tests indexed lookup behavior and precedence. |
system/Config/BaseConfig.php |
Implements indexing, caching, and shared lookup logic. |
Review effort: Balanced
Cache only the names of env vars found with live getenv() calls, per leaf, and re-read their values on each build, so removed or changed values are not reused. Leaves first seen on a later instance are now probed too. On Windows, probe the underscore forms live as well, so they keep matching case-insensitively. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… mod_php Under Apache mod_php, live getenv() reads SetEnv vars case-insensitively, so `SetEnv APP_BASEURL` matched `app_baseURL` in 4.8, but the indexed $_SERVER and getenv() array only hold the exact name. Probe the underscore forms live on any SAPI not known to match names exactly, keeping the cheaper path for CLI, FPM and FrankenPHP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
One last thing: in Worker Mode, the cache persists across requests, so changes made with |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, done in 348bcca. The upgrade note now says the limitation applies whether a variable is added, changed, or removed, and that in Worker Mode the cache persists across requests. It also gives the workaround: also set or unset the value in A correction to my earlier reply: changed or removed values are only re-read when |

Description
Fixes #10615
Without config caching, and on every request in FrankenPHP Worker Mode,
BaseConfig::__construct()checks every leaf property against the environment. That is 8 failedarray_key_exists()calls plus 4 failedgetenv()calls per leaf, even though most properties have no override.This PR indexes the environment once and reuses it:
$_ENV,$_SERVERandgetenv()are grouped by the text before their first.or_. Each class only looks at the keys that match its short or full prefix. A leaf is skipped unless one of its 4 candidate names exists.[$_ENV, $_SERVER]snapshot and is rebuilt when either changes (once per request in Worker Mode) or onBaseConfig::reset(). Copy-on-write makes the unchanged check cheap.getenv()doesn't list: PHP'sgetenv()array,$_ENVand$_SERVERall leave out OS env var names containing., a space or[. For example,app.baseURLset through Docker or systemd is only readable withgetenv('app.baseURL'). These names are probed withgetenv()the first time each leaf is seen. Only the names that exist are cached for the process, and their values are re-read on every build, so changed or removed values are never reused. Leaves that first appear on a later instance (for example, added by a constructor or registrar) are probed then. Leaves are only walked again when a class's property values change since its last build. That check is cheap because unchanged arrays share memory. This is the main difference from the issue's proposal, which would have dropped these names.getenv(): on Windows, and under Apache mod_php (wheregetenv()readsSetEnvvars, soSetEnv APP_BASEURLmatchesapp_baseURL), only livegetenv()calls match names case-insensitively. There the underscore forms are probed live too. This also applies to any SAPI not known to match exactly. The known-exact SAPIs arecli,phpdbg,cli-server,fpm-fcgiandfrankenphp: their livegetenv()only finds exact names that are also in$_SERVERor thegetenv()array, so they skip this step and avoid doubling thegetenv()calls on a cold build.getEnvValue()orinitEnvValue()keep the previous live lookups. This is detected with reflection once per class.$_ENVover$_SERVERovergetenv(), dot form over underscore form, and arrays only accept existing keys.getEnvValue()and the new path now share a single lookup.There is no new public or protected API.
Behavior change (listed in the changelog and
upgrade_480.rst):A bare
putenv(), without a matching$_ENV/$_SERVERwrite, that adds a new name after the first config class was built may not be seen untilBaseConfig::reset()runs. Values from.envare unaffected, becauseDotEnvalso writes$_ENVand$_SERVER.Benchmark
Time to build the 20 core config classes from the issue: PHP 8.3.23 CLI on macOS, OPcache off,
testingenvironment. Warm rows are the median of 9 rounds of 500 builds. Cold rows are the median of 41 fresh processes, each timing its first build after autoloading and Registrar discovery. "5 overrides" meansapp.baseURLplus fourdatabase.default.*keys in$_ENV,$_SERVERandputenv().$_SERVERchanges per build (Worker Mode)$_SERVERchanges per buildEach figure is the average of two back-to-back before/after rounds. These figures are for the CLI SAPI on macOS. On Windows, and under Apache mod_php or other SAPIs outside the known-exact list, a cold build makes 4
getenv()calls per leaf instead of 2.The cold gain is smaller because each class's first build still makes 2
getenv()calls per leaf for the names thatgetenv()doesn't list. I also tried keeping livegetenv()calls for those names on every build instead of caching them. That came out slower than the current code (about 0.59 ms), so the per-process cache is needed. Requests served from config caching (FactoriesCache) already skip this work and gain nothing.Benchmark scripts
Warm (
php bench-config-env.php /path/to/CodeIgniter4):Cold (
php bench-cold.php /path/to/CodeIgniter4 [overrides], run 41 times and take the median):Tests
New tests in
BaseConfigTestcover:$_ENV/$_SERVERwrites after a build being picked upreset()picking up a bareputenv()getenv()_getenv()-only value not being reusedThe full suite has the same failures as plain
4.8locally: SQLSRV and Predis are unavailable, and a fewFeatureTestTraittests fail only when run in the full suite.Checklist:
🤖 Generated with Claude Code