Re: [RFC] Scope
| From: | Larry Garfield | Date: | Wed, 28 Oct 2026 21:58:18 +0000 |
| Subject: | Re: [RFC] Scope | ||
| References: | 1 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-132950@lists.php.net to get a copy of this message | ||
On Mon, Oct 26, 2026, at 11:03 AM, Frode Børli wrote:
> Hi internals,
>
> I'd like to start the discussion on a new RFC:
>
> https://wiki.php.net/rfc/scope
>
> PHP has no object that represents a request. The request is the period
> between startup and shutdown, and its state lives in process-wide
> globals. That works while a process handles one request at a time, but
> not when one process serves many requests, and least of all when they
> overlap.
>
> The RFC adds Scope: an object that identifies a unit of work, such as
> an HTTP request, a queue job or a CLI command. Code can create a scope,
> run code in it, and ask which scope it is running in:
>
> (new Scope())->run($handler, $request);
>
> // anywhere in that request:
> $db = $this->connections[Scope::getCurrent()] ??= new PDO($dsn);
>
> Fibers and generators join the scope they are created in, so work a
> request spreads over several fibers still belongs to that request,
> whichever library or scheduler created them. Code that never creates a
> scope runs in the root scope and behaves exactly as today.
>
> The primary vote adds only the identity. Two secondary votes move
> engine state into the scope:
>
> A. the request superglobals and the session
> B. the default locale and timezone
>
> Other state (output buffers, error handlers, ini values) is left for
> later RFCs, so each can be discussed on its own.
>
> An extension implementing the RFC, including both secondary votes, is
> available for PHP 8.2 to 8.5:
>
> https://github.com/fiber-interop/ext-scope
> (pie install fiber-interop/ext-scope)
>
> A userland polyfill is fiber-interop/scope on Packagist. A php-src
> pull request will follow before the vote.
>
> I look forward to your feedback.
>
> Frode Børli
Minor thoughts as I read through it:
- Given that we now have PFA, which means turning pretty much any call into a closure involves just
adding ... to it, do we still want to have
run(callable, ...$args)? run(Closure
$c) would work just as well, and be callable like
$scope->run(doAThing(1, 2, 4, ...));
I'm not certain here, but it's worth considering if we can go a step further now.
- The docblock on Scope::bind() is horribly confusing and clumsy. It took me 5 reads to understand
what it was saying. I *think* the idea is to "change" the scope object inside a closure,
just like Closure::bind() changes the $this object inside a closure. Yes? (And returns new rather
than modifying in place, but...)
- "A scope keeps no link to the scope it was run from." So that fully precludes
getParentScope() and such, yes? What is the reason for avoiding that? (Not that I disagree, but I
want to understand why.)
- "The root scope is a single object that lives as long as the PHP request:" - I think you
mean "PHP process" here, as the process is not always isomorphic with an HTTP request.
(Which is the whole reason this proposal exists.)
- The phrasing and descriptions around closures and what scope they run in is quite confusing. It
really needs better examples. Am I correct in summarizing it as "a closure inherits the scope
of its callsiite, not creation site"?
- For secondary vote A (the superglobals), the example is very unclear. You're
useing $request and $handler, but those are never defined. It looks like you're
providing no alternative API or automation; really just an under the hood separation of $_GET based
on scope. While I can see the simplicity of that, it also means that basically every scope start
would need to do the same copying, unless it was absolutely certain there was no code that will be
run it that tries to read those superglobals.. Forget to do so and poof, some legacy garbage
function that reads $_POST directly now gets no data and you don't know why. This feels like a
footgun.
- The first mention of $GLOBALS is in the superglobal secondary vote section. Are globals and thus
$GLOBALS automatically scoped regardless? That should be clarified.
- Oh, that's mentioned further down in the "what follows scope" section. That should
come way earlier. :-) I am undecided if I agree globals should not follow the scope. That feels
like the sort of thing you would want to follow scope.
Overall, I think this is a promising approach. I like that it is narrowly and tightly focused.
However, I am still uneasy about how narrow it is. I can see the argument for "make your own
WeakMap and go away" as an approach. But I also fear that the result is we'll have a lot
of subtly-different and incompatible ways of handling that (say, a multi-fiber server like AmPHP, a
DI Container, and a specific service that does weird things for reasons), which could lead to
hard-to-diagnose bugs. I am undecided here.
As a concrete use case that I have been actively pondering of late: At work, we have an app that
switches which DB it connects to per-request, based on the Customer that is specified through the
domain name. (Right now it routes through an env var to a constant, but that I could change.) That
works fine in FPM mode, but would fail horribly in FrankenPHP Worker mode. Scopes seem like
they're the solution to that problem, but I'm not completely clear how they'd operate
in practice in that case. I wouldn't want a different DB connection to cause every service
downstream from it to be recreated on every soft-request, as that eliminates the main advantage of
worker mode. But the RFC examples are all doing a pseudo-global service locator, not something I am
able or willing to do. (I doubt I could coax Doctrine into working that way.) So what would it
looks like in a real dependency injection situation?
--Larry Garfield