[PHP-DEV] [RFC] Request lifecycle hooks for SAPIs with a worker mode

Hi internals,

Following the recent pre-RFC discussion
(https://news-web.php.net/php.internals/132960), I would like to open
the discussion on the "Request lifecycle hooks for SAPIs with a worker
mode" RFC:
PHP: rfc:request_lifecycle_hooks

As presented, this RFC proposes two optional module hooks, available
for extensions (no userland change), which are called between requests
served by a "worker" script.

I crafted a patch adding the hooks and support PHP functions. It
implements the hooks in the built-in standard, session and filter
extensions: GitHub - dunglas/php-src at request-boundary-hooks · GitHub

Here is also a patch showing how to use this new engine feature in
FrankenPHP: Comparing main...experiment/php-request-boundary-hooks · php/frankenphp · GitHub

Best,
--
Kévin Dunglas

Hey Kevin,

thanks for working on this. It would be really nice, from an extension
perspective, to have an explicit way to detect the logical request
boundaries in an explicit way.
I had one question when looking at the FrankenPHP example at

It seems that the lifecycle is actually starting with REND and
finishing with RBEGIN? Is that intended, I can see why it is the way
it is, it just feels odd.

So for example:

<?php
$handler = static function () {
    echo "request handled";
};
while (frankenphp_handle_request($handler)) {}
?>

The lifecycle of this code would look like:
- RINIT (plus all the things before)
- FrankenPHP would execute till the call to frankenphp_handle_request()
- then REND (to "shutdown"/end the dummy bootstrap request)
- wait for an actual HTTP request and then we enter the RBEGIN -> REND
loop as long as requests are served to that worker
- if that worker is about to shut down, frankenphp_handle_request()
would return false, but also do another call to RBEGIN, just none to
REND anymore before RSHUTDOWN

Kind regards
Florian

On Mon, Nov 9, 2026 at 3:25 PM Kévin Dunglas <kevin@dunglas.fr> wrote:

Hi internals,

Following the recent pre-RFC discussion
(https://news-web.php.net/php.internals/132960), I would like to open
the discussion on the "Request lifecycle hooks for SAPIs with a worker
mode" RFC:
PHP: rfc:request_lifecycle_hooks

As presented, this RFC proposes two optional module hooks, available
for extensions (no userland change), which are called between requests
served by a "worker" script.

I crafted a patch adding the hooks and support PHP functions. It
implements the hooks in the built-in standard, session and filter
extensions: GitHub - dunglas/php-src at request-boundary-hooks · GitHub

Here is also a patch showing how to use this new engine feature in
FrankenPHP: Comparing main...experiment/php-request-boundary-hooks · php/frankenphp · GitHub

Best,
--
Kévin Dunglas

https://dunglas.dev

Hi Florian,

Thanks for taking the time to read the RFC and review the patches.

Yes, it’s intended, and it’s already how FrankenPHP behaves today.

Because of how the engine works internally, the "dummy" request that
boots the worker script and the "logical" requests handled in the loop
don’t overlap: they run one after the other.

The full sequence in FrankenPHP is:

* RINIT ("dummy" request)
* userland bootstrap code
* main loop, frankenphp_handle_request() waits for a request (its
first call first runs REND to clean the request-specific state tied to
the "dummy" request):
   * RBEGIN (logical HTTP request)
   * callback
   * REND (logical HTTP request)
   * when the worker must stop, frankenphp_handle_request() calls
RBEGIN and returns false: this recreates a "dummy" request, because
the code after the loop can still write output, and
php_request_shutdown() needs an active request
* userland shutdown code
* RSHUTDOWN ("dummy" request)

For extensions, the rules are the same as today: what is set up in
RINIT or RBEGIN must be released in the next REND or RSHUTDOWN. A
debugger or a profiler will likely keep running the same code as today
in RSHUTDOWN, and an adapted version of it in the new REND. That’s by
design: existing extensions keep working unchanged, and a logical
request is now simply one that starts with RBEGIN and ends with REND.
Best,

Kévin