---
title: "RFC пропонує хуки життєвого циклу запиту для воркер-режиму PHP"
url: https://laravelukraine.com/blog/rfc-proponuje-xuki-zittjevogo-ciklu-zapitu-dlia-vorker-rezimu-php
date: 2026-11-14
---

# RFC пропонує хуки життєвого циклу запиту для воркер-режиму PHP

Воркер-режим PHP може отримати офіційні межі запиту. 9 листопада Кевін Дюнгла, автор FrankenPHP, виніс на [обговорення в internals](https://news-web.php.net/php.internals/133044) [RFC «Request lifecycle hooks for SAPIs with a worker mode»](https://wiki.php.net/rfc/request_lifecycle_hooks): два нові хуки, через які рушій і розширення дізнаються, де закінчується один логічний HTTP-запит і починається наступний. Для всіх, хто тримає Laravel в пам'яті через Octane, це спроба прибрати цілий клас помилок, які досі латали вручну.

### Воркер-режим простими словами

Класична модель PHP - «запустив і забув»: на кожен запит FPM чи Apache виконує скрипт з нуля, а після відповіді все, що скрипт створив, зникає. Автозавантажувач Composer, контейнер Laravel, конфігурація - усе будується заново щоразу. Повільно, зате надійно: стан одного запиту фізично не може дістатися до іншого, а витік пам'яті живе рівно до кінця запиту.

У воркер-режимі довгоживучий скрипт стартує разом із сервером, один раз завантажує застосунок і далі в циклі обслуговує запит за запитом. Так працює Laravel Octane: фреймворк лишається в пам'яті, і на кожен запит не треба знову піднімати сервіс-контейнер. Ціна - стан між запитами вже ніщо не ізолює, і за чистоту відповідає хтось інший.

Проблема, яку описує RFC, у тому, що з погляду PHP увесь цей цикл - один-єдиний запит. Хуки `RINIT` і `RSHUTDOWN`, якими розширення готуються до запиту і прибирають після нього, спрацьовують лише двічі: коли воркер стартує і коли зупиняється. Окремих HTTP-запитів між ними рушій не бачить.

### Які баги вже були

Тож FrankenPHP скидає стан запиту сам, [залазячи у внутрішні структури рушія](https://github.com/php/frankenphp/blob/7f50be9d86261bf7cff9b051ab4fb572e95020ed/frankenphp.c#L400-L646) і вбудованих розширень. RFC називає цей підхід крихким: внутрішній код скидання може змінитися в новій версії PHP без попередження, а кожен пропуск означає або витік стану між запитами, або помилку роботи з пам'яттю. Лише цього року FrankenPHP виправляв три такі випадки:

- [витік session ID іншим клієнтам](https://github.com/php/frankenphp/security/advisories/GHSA-v3ph-cgqh-r8p5) (високий рівень загрози). Коли розширення `session` підключене як спільний модуль (`extension=session`, як у пакетах RPM і Debian), код скидання сесії просто не потрапляв у збірку FrankenPHP. `session_start()` бачив ідентифікатор попереднього запиту, ігнорував cookie і віддавав клієнтові чужу сесію;
- [use-after-free потоків `php://temp`](https://github.com/php/frankenphp/security/advisories/GHSA-cm3q-jw2q-9rh5). FrankenPHP звільняв внутрішній потік `SplTempFileObject`, який пережив межу запиту, і наступне звернення до об'єкта читало вже звільнену пам'ять. Виправлено у FrankenPHP 1.13.4;
- [double free](https://github.com/php/frankenphp/security/advisories/GHSA-h764-4wh7-m7j2) після не-формового `POST` (JSON, XML), якщо воркер сам завершує цикл. Advisory прямо згадує Octane: він за замовчуванням перезапускає воркер кожні 500 запитів. Теж виправлено в 1.13.4, а саму причину в рушії [закрили для PHP 8.6](https://github.com/php/php-src/pull/22974).

Інші воркер-SAPI (Turbine, bext, worker API бібліотеки ext-php-rs) копіюють кожен свою частину коду ініціалізації та завершення запиту, і кожен щось пропускає. А сторонні розширення ловлять межі запиту самі: Xdebug і профайлер Datadog підміняють функції SAPI, трейсер Datadog, Tideways і New Relic (поки у відкритих pull request) перехоплюють `frankenphp_handle_request()`, Blackfire і Sentry обгортають запит в userland. Жоден із цих способів не працює з іншими воркер-SAPI.

Чому б просто не викликати `RSHUTDOWN` і `RINIT` між запитами? Бо ці хуки змішують стан, який треба скидати, зі станом, який скрипт має зберегти. `RSHUTDOWN` розширення `session` записує сесію, але заодно звільняє зареєстровані обробники збереження. Розділити одне від іншого можуть тільки мейнтейнери кожного розширення.

### Що пропонується

RFC додає два необов'язкові хуки модуля, які SAPI викликає між двома логічними запитами одного скрипта:

- `request_end` - звільнити стан завершеного запиту, не чіпаючи нічого, що ще досяжне з userland. На цей момент буфери виводу вже скинуті, а заголовки надіслані;
- `request_begin` - підготувати наступний запит, коли його заголовки й суперглобальні змінні вже доступні. Наприклад, для `session.auto_start` чи тригерів Xdebug.

Для самих SAPI є дві функції рушія, `php_request_begin()` і `php_request_end()`. Вони роблять те, що кожен воркер-SAPI зараз копіює у себе: вмикають і вимикають шар виводу, імпортують суперглобальні змінні нового запиту, запускають таймер виконання і викликають хуки. Цикл воркера зводиться до кількох рядків:

```c
  while (next_request()) {
      /* fill SG(request_info) and SG(server_context) */
      php_request_begin();
      /* call the userland handler */
      php_request_end();
  }
```

Межа скидання окреслена вузько: тільки стан, побудований з вхідного запиту або пов'язаний з відповіддю. Це суперглобальні змінні (крім `$_ENV`), тіло запиту, сесія і стан виводу. А от `mt_rand()`, `error_get_last()`, зміни через `putenv()`, кеш `stat`, налаштування з `ini_set()` і часовий пояс за замовчуванням лишаються спільними, як у довгоживучому CLI-скрипті. Хуки ніколи не звільняють того, до чого ще може дістатися код: об'єкти, ресурси, колбеки, автозавантажувачі.

Вбудовані розширення отримують хуки одразу: `standard` скидає URL rewriter (`output_add_rewrite_var()`), `session` записує й закриває сесію, зберігаючи обробники збереження, `filter` звільняє копії вхідних даних, які зараз просто течуть на кожному запиті. Сторонні розширення переходять на хуки у власному темпі, а старі компілюються без змін.

Для userland нового API немає. В RFC прямо сказано: на userland-код зміна не впливає і нічого в ньому не вимагає.

### Що це означає для Octane

Тут важливо, на якому сервері працює ваш Octane. RFC ділить рантайми на три групи:

- **FrankenPHP** (а також Turbine, bext і Rust-SAPI на ext-php-rs) сам є SAPI і може замінити власні копії коду на `php_request_begin()` і `php_request_end()`. Дюнгла вже показав [гілку FrankenPHP на нових хуках](https://github.com/php/frankenphp/compare/main...experiment/php-request-boundary-hooks): з неї зникає ручне скидання сесії, URL rewriter і більшість скопійованого коду рушія;
- **Swoole, RoadRunner і Workerman** крутять цикл в userland поверх CLI SAPI. SAPI вони не контролюють, тож викликати ці функції не можуть, і RFC їх напряму не стосується;
- **FPM, Apache, NGINX Unit** виконують один скрипт на запит і не бачать жодних змін.

Тобто виграють передусім ті, хто запускає Octane на FrankenPHP: менше коду, який ламається з кожною новою версією PHP, і менше шансів на ще один витік між запитами. Ваш код Laravel переписувати не доведеться, а масив `flush` у `config/octane.php` і скидання стану самого фреймворку лишаються як були. Хуки стосуються стану рушія й розширень, а не ваших синглтонів.

Окремо про сесії: Laravel має власний компонент сесій і не використовує `session_start()`, тож витік session ID насамперед зачіпав код на нативних сесіях PHP. Але приклад показовий: помилка ховалася не в застосунку, а в тому, як зібране розширення.

### Що це дасть профайлерам і APM

Для авторів розширень це головна новина. Xdebug, Datadog, Tideways, New Relic отримали б одну абстракцію, яка працює з будь-яким SAPI з воркер-режимом, замість окремих латок під FrankenPHP. Перехід на хуки дасть їм заодно й підтримку Rust-SAPI.

Крім того, хуки дозволяють відрізнити «головний» запит (сам воркер, від `RINIT` до `RSHUTDOWN`) від логічних запитів, які він обслуговує. RFC наводить приклад Xdebug: можна було б окремо відлагоджувати код, що завантажує застосунок і виконується між запитами, і окремо - один конкретний HTTP-запит, запущений його власними тригерами. Профайлер так само відправлятиме профіль на кожен логічний запит у `request_end`, а не один гігантський профіль на весь час життя воркера.

RFC також окремо розбирає зв'язок із [RFC Scope](https://wiki.php.net/rfc/scope) і доходить висновку, що пропозиції не конкурують: Scope дає ідентичність паралельним запитам, але не має точки, де запит закінчується. Якщо пройдуть обидві, `php_request_begin()` створюватиме новий scope.

### Статус і що робити зараз

Це пропозиція, а не ухвалене рішення. RFC має версію 0.1 і статус Under Discussion, голосування ще не призначене, а для ухвалення знадобиться дві третини голосів. Цільова версія - PHP 8.7. Обговорення в internals щойно почалося, тож деталі ще можуть змінитися. Паралельно в internals обговорюють і [RFC про закриття PECL у 2029 році](https://laravelukraine.com/blog/rfc-proponuje-zakriti-pecl-u-2029-roci-grafik-i-shho-robiti-zaraz).

Практично для Laravel-проєкту на Octane нічого міняти не треба, а до PHP 8.7 ще далеко. Що варто зробити вже зараз:

- якщо Octane працює на FrankenPHP, оновіться щонайменше до 1.13.4, де закриті use-after-free і double free;
- якщо ставите FrankenPHP з пакетів RPM чи Debian, де `session` - спільне розширення, перевірте [advisory про витік сесій](https://github.com/php/frankenphp/security/advisories/GHSA-v3ph-cgqh-r8p5) і свою версію;
- якщо пишете розширення для PHP або підтримуєте APM-агента, почитайте RFC і [реалізацію](https://github.com/dunglas/php-src/tree/request-boundary-hooks) та висловіться в треді, поки API ще можна змінити.
