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
Volker and I drafted a RFC:
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]