Summary
With OPcache enabled, a cached require performs no filesystem access — it executes cached opcodes. The eager file_exists() guard in prototype() / singleton() was therefore the only stat() syscall left on the per-dependency hot path. Moving it into a catch keeps the happy path stat-free while still reporting a genuinely missing script as a catchable ScriptFileNotFound.
Change
src-function/prototype.php, src-function/singleton.php:
try {
return require $file; // happy path: no stat(), cached opcodes
} catch (Throwable $e) {
if (! file_exists($file)) { // stat() only on failure
throw new ScriptFileNotFound($filePath, 0, $e);
}
throw $e; // file exists -> error came from inside the script
}
This relies on PHP 8 making a failed require a catchable Error/ErrorException (the project already requires PHP ^8.2). try/catch is zero-cost on the happy path, and putting file_exists() inside the catch distinguishes a missing file from an error thrown within the script without fragile message matching.
CompiledInjector::getInstance() keeps its file_exists() pre-check on purpose — it reports unbound interfaces as Unbound, is called once per top-level resolution (not per nested dependency), and the pre-check avoids a spurious warning on that path. Its redundant realpath($this->scriptDir) (the value is already canonicalised in the constructor) was removed.
Measurements
FakeCar graph (ctor + 5 setters + AOP + singleton mirrors), PHP 8.4, OPcache valid, steady-state per build:
|
compiled, per build |
before (eager file_exists()) |
~32.7 µs |
after (file_exists() in catch) |
~22.3 µs |
~30% faster, and ~2.1× faster than the reflection / serialized injector (~46 µs).
Why this is easy to mis-benchmark
opcache.file_update_protection (default 2s) refuses to cache files younger than that. A benchmark that compiles and immediately measures re-parses every require and shows the compiled injector at ~178 µs — a pure re-parse artifact. A sleep() does not help on the CLI (OPcache's age check uses the request start time). Back-date the generated scripts (touch) or set opcache.file_update_protection=0, and verify the OPcache hit rate.
Docs / benchmark
docs/performance.md — the three strategies, the OPcache prerequisite, and benchmarking pitfalls.
benchmark/di_benchmark.php — a self-validating benchmark that prints the OPcache hit rate, so a (valid) run is distinguishable from a re-parse artifact.
All checks pass (phpunit, phpcs, psalm, phpstan).
Summary
With OPcache enabled, a cached
requireperforms no filesystem access — it executes cached opcodes. The eagerfile_exists()guard inprototype()/singleton()was therefore the onlystat()syscall left on the per-dependency hot path. Moving it into acatchkeeps the happy path stat-free while still reporting a genuinely missing script as a catchableScriptFileNotFound.Change
src-function/prototype.php,src-function/singleton.php:This relies on PHP 8 making a failed
requirea catchableError/ErrorException(the project already requires PHP^8.2).try/catchis zero-cost on the happy path, and puttingfile_exists()inside thecatchdistinguishes a missing file from an error thrown within the script without fragile message matching.CompiledInjector::getInstance()keeps itsfile_exists()pre-check on purpose — it reports unbound interfaces asUnbound, is called once per top-level resolution (not per nested dependency), and the pre-check avoids a spurious warning on that path. Its redundantrealpath($this->scriptDir)(the value is already canonicalised in the constructor) was removed.Measurements
FakeCar graph (ctor + 5 setters + AOP + singleton mirrors), PHP 8.4, OPcache valid, steady-state per build:
file_exists())file_exists()incatch)~30% faster, and ~2.1× faster than the reflection / serialized injector (~46 µs).
Why this is easy to mis-benchmark
opcache.file_update_protection(default 2s) refuses to cache files younger than that. A benchmark that compiles and immediately measures re-parses everyrequireand shows the compiled injector at ~178 µs — a pure re-parse artifact. Asleep()does not help on the CLI (OPcache's age check uses the request start time). Back-date the generated scripts (touch) or setopcache.file_update_protection=0, and verify the OPcache hit rate.Docs / benchmark
docs/performance.md— the three strategies, the OPcache prerequisite, and benchmarking pitfalls.benchmark/di_benchmark.php— a self-validating benchmark that prints the OPcache hit rate, so a(valid)run is distinguishable from a re-parse artifact.All checks pass (phpunit, phpcs, psalm, phpstan).