[PHP-DEV] [RFC] Scope functions

Volker and I drafted a RFC:

https://wiki.php.net/rfc/scope-functions

Please consider it and share your feedback.

I hope it will alleviate pain around some of the most common forms of Closure usage which is “execute this now as part of the called function”, which currently can require a lot of “use ($variables)”.

For me the primary use case of use ($capturing) was always “I need this function later and want to explicitly document what escapes my function”. This, however, required this straightforward usage of Closures to also document every single usage of a variable. Which is really not that beneficial at all.

Thus the scope functions as proposed will be able to fill that gap in future.

Thank you,
Bob

Le 6 mai 2026 à 22:09, Bob Weinand bobwei9@hotmail.com a écrit :

Volker and I drafted a RFC:

https://wiki.php.net/rfc/scope-functions

Please consider it and share your feedback.

I hope it will alleviate pain around some of the most common forms of Closure usage which is “execute this now as part of the called function”, which currently can require a lot of “use ($variables)”.

For me the primary use case of use ($capturing) was always “I need this function later and want to explicitly document what escapes my function”. This, however, required this straightforward usage of Closures to also document every single usage of a variable. Which is really not that beneficial at all.

Thus the scope functions as proposed will be able to fill that gap in future.

Thank you,
Bob

Hi,

This is nice. As I understand it, this RFC could resolve problems that the Context Managers RFC tries to resolve in a simpler and more flexible way. (And it resolves other problems too, of course.)

Taking the first example from the Context Manager RFC:


using (file_for_write('file.txt') => $fp) {
foreach ($someThing as $value) {
fwrite($fp, serialize($value));
}
}

// implementable as:

function file_for_write(string $filename): ContextManager {

return new class($filename) implements ContextManager {

function __construct(private readonly string $filename) { }

private $fp;

function enterContext() {
$this->fp = @fopen($this->filename, 'w');
if (!$this->fp) {
throw new \RuntimeException('Couldn’t open file');
}
return $this->fp;
}

function exitContext(?\Throwable $e = null): ?\Throwable {
@fclose($this->fp);
return $e;
}
};
}

This can be rewritten as:


file_for_write('file.txt', fn($fp) {
foreach ($someThing as $value) {
fwrite($fp, serialize($value));
}
});

// implementable as (which is simpler: one function instead of a whole class):

function file_for_write (string $filename, callable $do_write): void {
$fp = @fopen($filename, 'w');
if (!$fp) {
throw new \RuntimeException('Couldn’t open file');
}
try {
$do_write($fp);
} finally {
@fclose($fp);
}
}

For those of us that abhor exceptions in case of recoverable failure, there is even more. With this RFC, one can easily return true/false (or whatever other signal) for success/failure, while Context Manager strongly leans towards the use of exceptions (although, of course, it remains possible to assign the outcome to a variable and to exit the context with break or goto):


$ok = file_for_write('file.txt', fn($fp) {
foreach ($someThing as $value) {
if (something_is_wrong_with($value))
return false;
fwrite($fp, serialize($value));
}
return true;
});

// implementable as (which is more flexible: exceptions are not the only type of signal):

#[\NoDiscard]

function file_for_write (string $filename, callable $do_write): bool {
$fp = @fopen($filename 'w');
if (!$fp) {
return false;
}
try {
return $do_write($fp);
} finally {
@fclose($fp);
}
}

—Claude

On 6 May 2026 21:09:45 BST, Bob Weinand <bobwei9@hotmail.com> wrote:

Volker and I drafted a RFC:

PHP: rfc:scope-functions

Hi Bob,

An I right in thinking that this is essentially equivalent to "automatic capture by closure", with an extra restriction on the lifetime of the resulting closure? It would be good to spell out in the RFC how this compares to other approaches, and other languages.

In particular, JavaScript's capture semantics are somewhat notorious as a source of confusion. A common example is creating closures in a loop which all use the same variable; as far as I can see, the proposed semantics would allow this as long as they were executed before the parent scope ends:

function parent_scope() {
    $values = range(1,10);

    $scoped_functions = [];
    foreach( $values as $v ) {
        $scoped_functions[] = fn($v) {
            echo "Chosen item is {$v}";
        };
    }
    
    run_random_callable($scoped_functions);
}

/**
 * @param callable[] $callables
 */
function run_random_callable($callables) {
    $pick = array_rand($callables);
    $callables[$pick]();
}

// Will always output 'Chosen item is 10'!
// The list of "scoped functions" will all actually refer to the same $v, updated in the loop
parent_scope();

I don't know if there's some way to make the lifetime even more restricted, so that the use cases of "immediate execution" are allowed, but this kind of code is not.

For something like "run this in a transaction", the closure is really acting like a poor man's "continuation": it creates two new stack frames (transaction wrapper, callback) when what you really want is to *interleave* the boilerplate and the case-specific code.

In that sense, hygienic macros are probably the "ideal" solution - everything is expanded inline into a single scope, and there's no Closure object which can be misused.

Context Managers could in fact be implemented as such a macro, and as I understand it, the implementation is basically doing that internally by manipulating ASTs.

I wonder if there's any "minimal" macro system which would allow this kind of inline boilerplate expansion, without needing to design an entire meta-language?

Regards,

Rowan Tommins
[IMSoP]

Hey Rowan,

Am 07.05.2026 um 12:52 schrieb Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk>:

On 6 May 2026 21:09:45 BST, Bob Weinand <bobwei9@hotmail.com> wrote:

Volker and I drafted a RFC:

PHP: rfc:scope-functions

Hi Bob,

An I right in thinking that this is essentially equivalent to "automatic capture by closure", with an extra restriction on the lifetime of the resulting closure? It would be good to spell out in the RFC how this compares to other approaches, and other languages.

I think the closest thing to what we propose here is GCC Nested Functions: Nested Functions (Using the GNU Compiler Collection (GCC))
They have similar lifetime semantics - cannot outlive their declaration scope and cannot be redeclared either (by design because they're named, but scoped function names like that are not a thing in PHP, hence we assign to a variable).

In particular, JavaScript's capture semantics are somewhat notorious as a source of confusion. A common example is creating closures in a loop which all use the same variable; as far as I can see, the proposed semantics would allow this as long as they were executed before the parent scope ends:

function parent_scope() {
   $values = range(1,10);

   $scoped_functions = [];
   foreach( $values as $v ) {
       $scoped_functions[] = fn($v) {
           echo "Chosen item is {$v}";
       };
   }

   run_random_callable($scoped_functions);
}

/**
* @param callable[] $callables
*/
function run_random_callable($callables) {
   $pick = array_rand($callables);
   $callables[$pick]();
}

// Will always output 'Chosen item is 10'!
// The list of "scoped functions" will all actually refer to the same $v, updated in the loop
parent_scope();

I don't know if there's some way to make the lifetime even more restricted, so that the use cases of "immediate execution" are allowed, but this kind of code is not.

Have you missed this following restriction and example in the RFC?

Re-evaluation: The same scope function declaration (same source code location) invalidates the previous instance. Should it still be called, an error is raised.

Any scope closure is only allocated a single slot, preventing recursion and double declaration. There are two choices: guaranteeing uniqueness (returning the active Closure again) or invalidating the previous Closure. We chose the second option to avoid the misconception that scope functions would capture the current variables. E.g. in foreach loops, it will always print the current active value and never the value at declaration time.

$closures = [];
for ($i = 0; $i < 3; $i++) {
    $closures[] = fn() { return $i; };
}
var_dump($closures[2]()); // int(3)

$closures[0](); // Error: Cannot call scope function: defining scope has exited

This restriction exists exactly because of that issue.

For something like "run this in a transaction", the closure is really acting like a poor man's "continuation": it creates two new stack frames (transaction wrapper, callback) when what you really want is to *interleave* the boilerplate and the case-specific code.

One thing which this does is interleaving with a well-defined boundary. return works and does not cross the whole function, just the callback.
You don't get a sudden goto or break skipping some of your code. The internal variables of the wrapper frame are isolated and don't leak to you. Argument values to the wrapper are properly type checked.

In that sense, hygienic macros are probably the "ideal" solution - everything is expanded inline into a single scope, and there's no Closure object which can be misused.

With "hygienic" you mean the macro has to produce full expressions, right, i.e. not C preprocessor style.

Context Managers could in fact be implemented as such a macro, and as I understand it, the implementation is basically doing that internally by manipulating ASTs.

I wonder if there's any "minimal" macro system which would allow this kind of inline boilerplate expansion, without needing to design an entire meta-language?

You wonder rightly, but the main problem is the nature of the language/implementation. How would you compile that?
op_arrays are fixed at compile time and not going to change afterwards.

So, you can either try calling functions at compile time to manipulate a functions code (let's say e.g. all !-suffixed function names get called at compile time with some sort of token stream or AST, whatever) or invent very clever hacks to somehow generate op_array snippets on the fly at runtime.
That once-per-request nature of PHP also makes it very difficult to efficiently cache such code (unless you assume every invocation of that function will always output the same, which you cannot even rely on, because every request could invoke a different file defining the same macro etc.).

You will also get interesting behaviours with jumps, e.g. a goto will suddenly ... jump over a bunch of logic, so any macro will have to do try/finally. while loops inside that macros, do they count towards the break/continue stack etc.?

It's a big rabbit hole. You'll find that it's not impossible to do, but it has quite a few trade offs and possibly odd choices to make.

I am not opposed at all to such a consistent macro system though.
... but I would not make a transaction like that a macro. For inlining variables in a html or sql snippet with proper escaping, definitely great.
To me, macros should meaningfully transform their contained code rather than wrapping a bit of logic around, which would be covered by this RFC.
It's complimentary, not a replacement.

Thanks,
Bob

On 7 May 2026 13:16:27 BST, Bob Weinand <bobwei9@hotmail.com> wrote:

I don't know if there's some way to make the lifetime even more restricted, so that the use cases of "immediate execution" are allowed, but this kind of code is not.

Have you missed this following restriction and example in the RFC?

Re-evaluation: The same scope function declaration (same source code location) invalidates the previous instance. Should it still be called, an error is raised.

I skimmed past that section, and didn't realise its significance, thinking it was just covering off edge cases of the implementation.

I think the core principle behind the various restrictions needs to be made much clearer in the summary of the proposal, because up to that point all the examples just look identical to normal closures with automatic capture by reference.

For similar reasons, perhaps a different keyword would be better, so that users don't have a wrong expectation of what it means.

Regards,

Rowan Tommins
[IMSoP]

Thanks for this RFC, here are my 2cents:

I think the current syntax is too close to current short closures, and since it allows multiline closures, it would imply that by default all multiline short closures are scoped, which is not intuitive since traditional closures don't capture by default.
Instead, I think replacing the `fn () { }` syntax to add a keyboard indicating it inherits the scope would drastically help making it more intuitive.

I would imagine instead something like this:

 $externalVar = 0;
 $closure1 = scoped fn \(\) =&gt; $newVar1 = $externalVar\+\+;
 $closure2 = scoped function \(\) \{
     return $newVar2 = $externalVar\+\+;
 \};
 $closure1\(\);
 $closure2\(\);
 var\_dump\($newVar1\); // int\(0\)
 var\_dump\($newVar2\); // int\(1\)
 var\_dump\($externalVar\); // int\(2\)

This way, for the first case, the only new information that `scoped` brings is the potential existence of `$newVar1` after the closure's execution, and for the second closure, it makes it explicit that all variables in the closure's body are shared with the parent scope.

WDYT?

Le 06/05/2026 à 22:09, Bob Weinand a écrit :

Volker and I drafted a RFC:

PHP: rfc:scope-functions

Please consider it and share your feedback.

I hope it will alleviate pain around some of the most common forms of Closure usage which is "execute this now as part of the called function", which currently can require a lot of "use ($variables)".

For me the primary use case of use ($capturing) was always "I need this function later and want to explicitly document what escapes my function". This, however, required this straightforward usage of Closures to also document every single usage of a variable. Which is really not that beneficial at all.

Thus the scope functions as proposed will be able to fill that gap in future.

Thank you,
Bob

On Thu, May 7, 2026, at 5:52 AM, Rowan Tommins [IMSoP] wrote:

For something like "run this in a transaction", the closure is really
acting like a poor man's "continuation": it creates two new stack
frames (transaction wrapper, callback) when what you really want is to
*interleave* the boilerplate and the case-specific code.

In that sense, hygienic macros are probably the "ideal" solution -
everything is expanded inline into a single scope, and there's no
Closure object which can be misused.

Context Managers could in fact be implemented as such a macro, and as I
understand it, the implementation is basically doing that internally by
manipulating ASTs.

Correct. Absent a macro system like Rust, it's just a one-off compiler "macro" that turns into try-catch-finally.

I wonder if there's any "minimal" macro system which would allow this
kind of inline boilerplate expansion, without needing to design an
entire meta-language?

The PFA implementation does generate new opcodes at runtime and cache them. So such things are possible. Wise to generalize or not is an entirely separate question, though. :slight_smile:

--Larry Garfield

On 07/05/2026 13:16, Bob Weinand wrote:

I am not opposed at all to such a consistent macro system though.
... but I would not make a transaction like that a macro. For inlining variables in a html or sql snippet with proper escaping, definitely great.
To me, macros should meaningfully transform their contained code rather than wrapping a bit of logic around, which would be covered by this RFC.
It's complimentary, not a replacement.

Thinking about this further, I agree that full-blooded macros would be overkill for this case, but my starting point was that closures don't feel like the right solution either - they're a different feature being bent into shape.

So rather than starting with a Closure, and taking things away, I was trying to think of ways to start with a simple block of code, and build it up.

For instance, the array_walk example in the RFC is just an ugly (and probably inefficient) way of writing a foreach loop:

array_walk($numbers, fn($number) {
$count = bcadd($count, '1');
$sum = bcadd($sum, $number);
});

foreach($numbers as $number) {
$count = bcadd($count, '1');
$sum = bcadd($sum, $number);
}

So, what would it look like to generalise that, so we could do the same for other scenarios, like async() and transaction()?

A simple first step would be something that could take a block of code, and say when to execute it:

custom_block transaction($dbConnection) {
try {
$dbConnection->beginTransaction();
__execute_block_body();
}
catch (\Throwable $e) {
$dbConnection->rollbackTransaction();
throw $e;
}
$dbConnection->commitTransaction();
}

transaction($myDb) {
$myDb->query('UPDATE ...');
}

If the block body is never represented as a variable, there's no need to define what happens when it outlives scope, or the user tries to clone it, rebind it, etc.

And if the calling code doesn't look like it's creating a Closure, users won't have any wrong expectations about variable scopes or lifetimes.

Onto that, we can add syntax to push values into the block, e.g. using the "as" keyword like in "foreach" (example based on the "Generator decorator managers" section at the end of the Context Managers RFC):

custom_block opening($filename) {
$f = fopen($filename, "r");
if (!$f) {
throw new Exception("fopen($filename) failed");
}
try {
__execute_block_body($f);
} finally {
fclose($f);
}
}

opening(__FILE__ as $f) {
var_dump($f);
}

And some way to pull values out, e.g. a "break with" keyword:

custom_block transaction(DBConnection $conn) {
$conn->beginTran();
try {
if (__execute_block_body() === DBConnection::TRANSACTION_ABORT) {
$conn->rollbackTran();
return;
}
} catch (\Throwable $e) {
$conn->rollback();
throw $e;
}
$conn->commitTran();
}

transaction($connection) {
$affectedRows = $connection->query("UPDATE ...");
if ($affectedRows === 0) {
break with DBConnection::TRANSACTION_ABORT;
}
// ...
}

I'm sure there are details here that wouldn't quite work as expressed, but hopefully it explains what I meant by wanting something closer to macros than closures.

Regards,

--
Rowan Tommins
[IMSoP]

On Thu, 7 May 2026 at 19:03 Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

On 07/05/2026 13:16, Bob Weinand wrote:

I am not opposed at all to such a consistent macro system though.
… but I would not make a transaction like that a macro. For inlining variables in a html or sql snippet with proper escaping, definitely great.
To me, macros should meaningfully transform their contained code rather than wrapping a bit of logic around, which would be covered by this RFC.
It’s complimentary, not a replacement.

Thinking about this further, I agree that full-blooded macros would be
overkill for this case, but my starting point was that closures don’t
feel like the right solution either - they’re a different feature being
bent into shape.

So rather than starting with a Closure, and taking things away, I was
trying to think of ways to start with a simple block of code, and build
it up.

For instance, the array_walk example in the RFC is just an ugly (and
probably inefficient) way of writing a foreach loop:

array_walk($numbers, fn($number) {
$count = bcadd($count, ‘1’);
$sum = bcadd($sum, $number);
});

foreach($numbers as $number) {
$count = bcadd($count, ‘1’);
$sum = bcadd($sum, $number);
}

So, what would it look like to generalise that, so we could do the same
for other scenarios, like async() and transaction()?

A simple first step would be something that could take a block of code,
and say when to execute it:

custom_block transaction($dbConnection) {
try {
$dbConnection->beginTransaction();
__execute_block_body();
}
catch (\Throwable $e) {
$dbConnection->rollbackTransaction();
throw $e;
}
$dbConnection->commitTransaction();
}

transaction($myDb) {
$myDb->query(‘UPDATE …’);
}

If the block body is never represented as a variable, there’s no need to
define what happens when it outlives scope, or the user tries to clone
it, rebind it, etc.

And if the calling code doesn’t look like it’s creating a Closure, users
won’t have any wrong expectations about variable scopes or lifetimes.

Onto that, we can add syntax to push values into the block, e.g. using
the “as” keyword like in “foreach” (example based on the “Generator
decorator managers” section at the end of the Context Managers RFC):

custom_block opening($filename) {
$f = fopen($filename, “r”);
if (!$f) {
throw new Exception(“fopen($filename) failed”);
}
try {
__execute_block_body($f);
} finally {
fclose($f);
}
}

opening(FILE as $f) {
var_dump($f);
}

And some way to pull values out, e.g. a “break with” keyword:

custom_block transaction(DBConnection $conn) {
$conn->beginTran();
try {
if (__execute_block_body() === DBConnection::TRANSACTION_ABORT) {
$conn->rollbackTran();
return;
}
} catch (\Throwable $e) {
$conn->rollback();
throw $e;
}
$conn->commitTran();
}

transaction($connection) {
$affectedRows = $connection->query(“UPDATE …”);
if ($affectedRows === 0) {
break with DBConnection::TRANSACTION_ABORT;
}
// …
}

That looks like a function (aka closure) with a lot of weird extra steps. It’s been years, several proposals and multiple demonstrations on how arrow function syntax can be expanded to be useful with multi-statements. So much gymnastics goes on to deny such a simple and elegant syntax.