Request-scope и Swoole
request-scope — единственное место, где важен рантайм. При традиционной модели
«процесс на запрос» это просто синглтон; под Swoole, где один воркер обслуживает тысячи
параллельных запросов, он обязан изолировать экземпляр каждого запроса — и делает это через
контекст корутины.
Проблема, которую создаёт Swoole
Под PHP-FPM «один запрос = один процесс»: singleton, хранящийся в памяти процесса,
автоматически пер-запросный, потому что процесс умирает в конце запроса. Ничто не утекает.
Под Swoole это предположение ломается. Один долгоживущий воркер обслуживает много запросов
параллельно, все разделяют одну память процесса. singleton, хранящий текущего
пользователя, был бы виден каждому запросу «в полёте» одновременно — баг с утечкой данных.
request-scope существует, чтобы дать каждому параллельному запросу свой экземпляр без этой
утечки.
Как работает изоляция
Swoole запускает каждый запрос в собственной корутине, и у каждой корутины есть
изолированный объект контекста (Coroutine::getContext()), уничтожаемый по завершении
корутины. Winter DI хранит request-scoped экземпляры там, под приватным ключом __di:
Процесс воркера (общая память)
├─ Корутина #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].
Связанное
- Scope’ы — три scope’а и матрица безопасности
- Жизненный цикл разрешения — где читается/пишется кэш корутины
- Кэш рефлексии — другая оптимизация горячего пути Swoole