Пакет · di

Кэш рефлексии

Автовайринг управляется рефлексией, а рефлексия не бесплатна. ReflectionCache строит каждый объект рефлексии один раз и переиспользует его всё время жизни процесса — разница между «дёшево» и «линейной ценой на запрос» в Swoole-воркере.

Почему это важно

Модель стоимости различается по рантайму:

text
FPM-воркер:    1 запрос  → 1 построение рефлексии   (нечего амортизировать)
Swoole-воркер: N запросов → 1 построение рефлексии   (N-1 попаданий в кэш)

Под FPM каждый запрос — свежий процесс, поэтому new ReflectionClass() случается один раз и умирает с запросом — кэширование даёт мало. Под Swoole один воркер обслуживает тысячи запросов в том же процессе; без кэша каждый запрос перестраивал бы тот же граф рефлексии, и эта цена растёт линейно с трафиком. ReflectionCache гарантирует, что граф строится один раз на воркер, независимо от числа запросов.

Что кэшируется

Четыре статических карты, каждая с ключом и ленивым заполнением при первом обращении:

Метод Кэширует Ключ
classOf($class) ReflectionClass имя класса
enumOf($enum) ReflectionEnum имя enum
method($class, $method) ReflectionMethod class::method
parameters($class, $method) ReflectionParameter[] class::method (делегирует method())

parameters() переиспользует ту же запись, что строит method(), поэтому запрос любого из них не дублирует работу.

Как это использует резолвер

Движок DI (ReflectionResolver) опирается на кэш в трёх точках, плюс второй слой собственной мемоизации для извлечённых метаданных параметров (имя, тип, атрибуты, значения по умолчанию) — так форма конструктора вычисляется один раз, а затем читается из обычного массива.

Шаг резолвера Вызов кэша
resolve() → параметры конструктора classOf($class)->getConstructor()
call() method($instance::class, $method)
injectProperties() classOf($instance::class)->getProperties()

Этот двухуровневый дизайн — кэшированные объекты рефлексии и кэшированные массивы параметров — и есть причина, по которой горячий make() на уже виденном классе почти не делает работы с рефлексией.

Это публичная утилита

ReflectionCache не внутренний. Любой слой на основе рефлексии — HTTP-контроллеры, CLI-диспетчеры, резолверы параметров, читающие свои атрибуты — может разделять тот же кэш вместо построения параллельного:

php
use Flytachi\Winter\DI\ReflectionCache;

// Резолвер параметров HTTP, читающий #[PathVariable], #[RequestBody], ...
$params = ReflectionCache::parameters($controllerClass, $action);
foreach ($params as $param) {
  if ($attr = $param->getAttributes(PathVariable::class)[0] ?? null) {
      // разрешить path-переменную
  }
}

// Вызвать action контроллера
$method = ReflectionCache::method($controllerClass, $action);
$method->invokeArgs($controller, $resolvedArgs);

Потокобезопасность

Кэш использует static-массивы. В FPM и CLI у каждого процесса своя память — нет разделения, нет блокировок. Под Swoole все корутины в воркере разделяют память процесса, но объекты рефлексии только для чтения после создания, поэтому параллельные чтения безопасны без блокировок. Худший случай при гонке — две корутины оба строят одну и ту же запись по разу; результат идентичен и идемпотентен.

Инвалидация не нужна

Формы классов не меняются в рантайме, поэтому кэшированная рефлексия не устаревает в пределах процесса. Вытеснения намеренно нет — кэш ограничен числом различных классов, которых касается воркер, а оно конечно.

Связанное