Le 29/03/2026 à 13:14, Tim Düsterhus a écrit :
// ...
I'm sorry that I'm arriving super late in the process, but I've
subscribed to the mailing list only recently.
To give here my 2cents:
I think this RFC addresses something good (scoped code in the middle of
a bigger chunk of code), but does it maybe a bit too bloated-ly.
(As a side-note, I think the keyword should be replaced for "scoped"
instead of "using", because "using" seems to refer mostly to the
starting point of the code chunk, instead of the actual code inside it.)
TL;DR:
If this RFC is replaced with a "simpler" implementation relying only on
Closures, I think this brings these advantages:
- The engine already has everything for it, no need for new internal
classes. Maybe a new boolean in ReflectionFunction like "isScopedBlock"
that would indicate the closure was created from a "scoped" keyword, but
so far I see no real use-case for this (yet, maybe you think about one
case, feel free to tell)
- No keyword misunderstandings (continue, break...)
- Returns are possible by design, as well as return types
- The closure is auto-destructed after execution, therefore freeing (or
stacking for gc) the memory, as well as decreasing the number of
references for objects if any parent-scope-created object variable was
injected in the closure (so basically, refcount will be N before
executing, N+1 during scope execution, and N again after scope finished
executing), unless the returned variable contains references to the
internal scope (which isn't a good idea, and defeats the purpose of
scoped code anyway).
- Native support for try/catch blocks
- Since you can specify return types, you can also handle Generators
from inside the scope (but will need to return it into a variable, of
course, but it's still not a good idea, and an external function would
be more appropriate, closure or not)
// end of TL;DR, here comes the explanations:
Let me explain:
The RFC proposes new classes and a new keyword, this keyword implies the
existence of a sub-context as a "starter" etc etc, well, you read it in
the RFC already, hopefully.
Meanwhile, PHP has had something quite cool for "scoping" some code
execution, and it is even able to somehow /prepare/ this scope
/before/ it is executed.
Of course, I mean Closures.
When I see this:
using (new Manager()) {
print "Hello world\n";
}
// Will compile into approximately the equivalent of this:
$__mgr = new Manager();
$__closed = false;
$__mgr->enterContext();
try {
print "Hello world\n";
} catch (\Throwable $e) {
$__closed = true;
$__ret = $__mgr->exitContext($e);
if ($__ret instanceof Throwable) {
throw $e;
}
} finally {
if (!$__closed) {
$__mgr->exitContext();
}
unset($__closed);
unset($__mgr);
}
I can't help but think "This is too much".
The Manager class comes here with lots of additional code, and even the
"ContextManager" attribute proposed later adds even more code to the
execution.
I think the same example could be fixed with a closure like this:
using {
print "Hello ".$world."!\n";
}
// Will compile into approximately the equivalent of this:
(function () use ($world) { print "Hello ".$world."!\n"; })();
The advantage of using closures is that the engine already has
everything needed for all possible use-cases, and the key word would
mostly be syntactic sugar to create a directly-executed closure.
In the different use-cases presented here, I can already imagine a lot
of things that would be simplified.
Here are some examples:
scoped {
echo "Hello world!";
}
// Compiles into:
(function () {
echo "Hello world!";
})();
scoped {
echo "Hello ".$world."!";
}
// Compiles into:
(function () use ($world) {
echo "Hello ".$world."!";
})();
And since we are inside a closure, we can assign this to a variable too:
$result = scoped: bool {
if (my_condition($input)) {
// do something.
return false;
}
return true;
}
// Compiles into:
$result = (function () use ($world): bool {
if (my_condition($input)) {
// do something.
return false;
}
return true;
})();
$iterator = scoped: Generator {
$h = fopen($sourceFile, 'rb+');
$headers = fgetcsv($h);
while ($row = fgetcsv($h)) {
yield array_combine($headers, $row);
}
fclose($h);
}
// Compiles into:
$iterator = (function () use ($sourceFile): bool {
$h = fopen($sourceFile, 'rb+');
$headers = fgetcsv($h);
while ($row = fgetcsv($h)) {
yield array_combine($headers, $row);
}
fclose($h);
})();
It already fixes the Exception/Throwable issue, because it's "just" a
closure, therefore one can surround it with try/catch:
try {
scoped {
some_task_that_might_throw();
}
} catch (Throwable $e) {
// Your stuff
}
// Compiles into:
try {
(function () {
some_task_that_might_throw();
})();
} catch (Throwable $e) {
// Your stuff
}
Maybe it's even possible to "sugar" it to this:
try scoped {
some_task_that_might_throw();
} catch (Throwable $e) {
// Your stuff
}
(that would be great, actually)
And by design, it also fixes the other keywords that might not be
exactly "intuitive" like "continue" or "break": if some code is scoped,
it means it /should not/ know about its parent execution context,
therefore neither "continue" nor "break" would have any effect on the
parent contexts. And since that code is written in a closure, writing
"continue" or "break" will throw a fatal error "break|continue not in
the 'loop' or 'switch' context", *because it is scoped*. Sure, this
removes the ability to interact with the parent context, but IMO this is
for the best: I can't imagine how strange the code using this syntax
will become over time, so preventing misuse might help understanding
better the real use-cases.
One possible remaining thing that should be documented (because it's
more obvious for closures, but maybe less with a keyword that wraps a
closure), is about shadowing variables.
This implies something like this:
$world = 'world';
scoped {
if ($world) {
$world = 'shadowed';
}
echo "Hello ".$world."!"; // "Hello shadowed!"
}
echo "Hello ".$world."!"; // "Hello world!"
Rust does this already with scoping blocks (some docs here:
https://doc.rust-lang.org/rust-by-example/variable_bindings/scope.html )
However, this does *not* shadow object variables, because they are
always passed by reference:
$parameters = new stdClass();
$parameters->key = 'world';
scoped {
$parameters->key = 'shadowed';
echo "Hello ".$parameters->key."!"; // "Hello shadowed!"
}
echo "Hello ".$parameters->key."!"; // "Hello shadowed!" <==
"$parameters" is an object, therefore its reference was updated
inside the scope.
*Note: *there /could/ be a built-in feature in the "scoped" keyword that
does copy-on-write for objects, to make sure any object updated in the
scope is not updated outside of it, but this implies a whole lot of
problems I don't think anyone wants to be involved with. Basically:
objects cloning. That's for the people that might comment about it, and
I think this should not be implemented, and the original behavior of
closures should be kept as-is. It leaves less work on the engine (and
the core devs), and is less prone to misunderstandings.
This can also nest scoped calls (even though it's ugly, it's an example):
$text = "world";
scoped {
$text = "first scope";
echo "Hello ".$text."!"; // "Hello first scope!"
scoped {
$text = "second scope";
echo "Hello ".$text."!"; // "Hello second scope!"
scoped {
echo "Hello last scope!";
}
}
echo "Hello again, ".$text."!"; // "Hello again, first scope!"
}
echo "Hello ".$text."!"; // "Hello world!"
// Compiles to this:
$text = "world";
(function () use ($text) {
$text = "first scope";
echo "Hello ".$text."!"; // "Hello first scope!"
(function () use ($text) {
$text = "second scope";
echo "Hello ".$text."!"; // "Hello second scope!"
(function () {
echo "Hello last scope!";
})();
})();
echo "Hello again, ".$text."!"; // "Hello again, first scope!"
})();
echo "Hello ".$text."!"; // "Hello world!"
And I'm even thinking about further usages similar to "move" or "async"
blocks in Rust, like "scoped foreach", "scoped while" or things like that:
scoped do {
$var = 1;
// ...
} while (some_check());
// Variable "$var" does not exist here
// It would compile to this:
do {
(function () use ($variable) {
$var = 1;
// ...
})();
} while (some_check());
// Or even:
scoped do {
if (isset($item)) {
// Do something
} else {
// Maybe throw?
}
} while ($item = some_check());
// Variable "$item" does not exist here
// It would compile to this:
scoped do {
(function () use ($item) {
if (isset($item)) {
// Do something
} else {
// Maybe throw?
}
})();
unset($item);
} while ($item = some_check());
// Variable "$item" does not exist here anymore
I could go on even more, but I think that, overall, the engine already
has scoping capabilities thanks to Closures, and it wouldn't need too
many new features in the core to implement this :)