Reflection cache
Autowiring is reflection-driven, and reflection isn’t free. ReflectionCache builds
each reflection object once and reuses it for the life of the process — the difference between
“cheap” and “linear cost per request” in a Swoole worker.
Why it matters
The cost model differs by runtime:
FPM worker: 1 request → 1 reflection build (nothing to amortise)
Swoole worker: N requests → 1 reflection build (N-1 cache hits)Under FPM each request is a fresh process, so new ReflectionClass() happens once and dies
with the request — caching buys little. Under Swoole a single worker handles thousands of
requests in the same process; without a cache, every request would rebuild the same reflection
graph, and that cost grows linearly with traffic. ReflectionCache ensures the graph is built
once per worker, regardless of request count.
What it caches
Four static maps, each keyed and populated lazily on first access:
| Method | Caches | Key |
|---|---|---|
classOf($class) |
ReflectionClass |
class name |
enumOf($enum) |
ReflectionEnum |
enum name |
method($class, $method) |
ReflectionMethod |
class::method |
parameters($class, $method) |
ReflectionParameter[] |
class::method (delegates to method()) |
parameters() reuses the same entry method() builds, so asking for either doesn’t duplicate
work.
How the resolver uses it
The DI engine (ReflectionResolver) leans on the cache at three points, plus a second layer of
its own memoisation for the extracted parameter metadata (name, type, attributes, defaults) —
so a constructor’s shape is computed once and thereafter read from a plain array.
| Resolver step | Cache call |
|---|---|
resolve() → constructor params |
classOf($class)->getConstructor() |
call() |
method($instance::class, $method) |
injectProperties() |
classOf($instance::class)->getProperties() |
That two-tier design — cached reflection objects and cached parameter arrays — is why a hot
make() on an already-seen class does almost no reflection work.
It’s a public utility
ReflectionCache isn’t internal. Any reflection-based layer — HTTP controllers, CLI
dispatchers, parameter resolvers reading their own attributes — can share the same cache
instead of building a parallel one:
use Flytachi\Winter\DI\ReflectionCache;
// HTTP parameter resolver reading #[PathVariable], #[RequestBody], ...
$params = ReflectionCache::parameters($controllerClass, $action);
foreach ($params as $param) {
if ($attr = $param->getAttributes(PathVariable::class)[0] ?? null) {
// resolve the path variable
}
}
// Invoke a controller action
$method = ReflectionCache::method($controllerClass, $action);
$method->invokeArgs($controller, $resolvedArgs);Thread safety
The cache uses static arrays. In FPM and CLI each process has its own memory — no sharing, no
locking. Under Swoole all coroutines in a worker share the process memory, but reflection
objects are read-only after creation, so concurrent reads are safe without locks. The worst
case under a race is two coroutines both building the same entry once; the result is identical
and idempotent.
No invalidation needed
Class shapes don’t change at runtime, so cached reflection never goes stale within a process. There is intentionally no eviction — the cache is bounded by the number of distinct classes the worker touches, which is finite.
Related
- Resolution lifecycle — where reflection feeds construction
- Request scope & Swoole — the other per-worker optimisation
- API reference —
ReflectionCachemethods