Пакет · di

Request-scope и Swoole

request-scope — единственное место, где важен рантайм. При традиционной модели «процесс на запрос» это просто синглтон; под Swoole, где один воркер обслуживает тысячи параллельных запросов, он обязан изолировать экземпляр каждого запроса — и делает это через контекст корутины.

Проблема, которую создаёт Swoole

Под PHP-FPM «один запрос = один процесс»: singleton, хранящийся в памяти процесса, автоматически пер-запросный, потому что процесс умирает в конце запроса. Ничто не утекает.

Под Swoole это предположение ломается. Один долгоживущий воркер обслуживает много запросов параллельно, все разделяют одну память процесса. singleton, хранящий текущего пользователя, был бы виден каждому запросу «в полёте» одновременно — баг с утечкой данных. request-scope существует, чтобы дать каждому параллельному запросу свой экземпляр без этой утечки.

Как работает изоляция

Swoole запускает каждый запрос в собственной корутине, и у каждой корутины есть изолированный объект контекста (Coroutine::getContext()), уничтожаемый по завершении корутины. Winter DI хранит request-scoped экземпляры там, под приватным ключом __di:

text
Процесс воркера (общая память)
├─ Корутина #1  ── getContext()['__di'] ── AuthContext(user: Ada)
├─ Корутина #2  ── getContext()['__di'] ── AuthContext(user: Linus)
└─ Корутина #3  ── getContext()['__di'] ── AuthContext(user: Grace)
   каждый запрос видит только свой экземпляр; контекст освобождается по завершении корутины

Два момента в жизненном цикле разрешения касаются этого:

  • Чтение — перед построением request-scoped абстракта (и без overrides) make() смотрит в корзину __di текущей корутины и возвращает уже лежащий там экземпляр.
  • Запись — после построения экземпляр записывается обратно в ту же корзину, чтобы остальная часть запроса переиспользовала его.

Обнаружение намеренно узкое: путь корутины выбирается, только если ext-swoole загружен и код выполняется внутри корутины (Coroutine::getCid() > 0).

Запасной вариант: FPM и CLI

Когда активной корутины нет — обычный FPM, CLI или код Swoole вне корутины — контейнер пишет экземпляр в свой кэш на уровне процесса, ровно как синглтон. Это корректно именно потому, что в этих рантаймах один процесс обслуживает один запрос.

Рантайм Где живёт request-scoped экземпляр Эффективное время жизни
Swoole (в корутине) Coroutine::getContext()['__di'] на корутину, освобождается по её завершении
FPM кэш на уровне процесса на процесс = на запрос
CLI кэш на уровне процесса на процесс = на команду

Ручная очистка не нужна

Вы никогда не очищаете кэш запроса сами. Под Swoole контекст корутины отбрасывается по её завершении; под FPM/CLI процесс завершается. Оба автоматически освобождают request-scoped экземпляры.

Практические рекомендации

  • Помещайте изменяемое пер-запросное состояние (контекст авторизации, текущий пользователь, unit of work, пер-запросные счётчики) в request-scope — никогда в singleton под Swoole.
  • Держите stateless-сервисы (репозитории, пулы соединений, читатели конфигурации) как singleton — разделять их между корутинами безопасно и дёшево.
  • Используйте transient для одноразовых объектов с состоянием (билдеры запросов, DTO), которые не должны быть общими даже в рамках одного запроса.

Полная таблица рекомендаций — на странице Scope’ы.

Классическая утечка Swoole

Самый частый баг — сервис #[Singleton], хранящий текущего пользователя или id запроса. Он работает в FPM-тестах и утекает между параллельными запросами в продакшн-Swoole. Когда класс хранит что-либо пер-запросное, он должен быть #[Request].

Связанное