Package · di

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:

text
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:

php
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.