Larry,
Am 2026-03-22 19:19, schrieb Larry Garfield:
Arnaud and I have made a number of changes to the RFC that should make
it sleaker and more consistent. The notable ones (that impact
behavior) are as follows:
“Sleaker” is not a word my dictionary understands. Was this a typo for
“sleeker” in the sense of “refined”?
1. We went back and forth on the `continue` question several times,
before coming to the conclusion that `continue` is a tool for looping
structures only. That `switch` also uses it is just `switch` being
silly because reasons, and there is no reason `using` must inherit its
weirdness. Therefore, `continue` inside a `using` block now means
nothing at all. `continue` will ignore it, the same way it ignores an
`if` statement.
I fail to see how “making break; and continue; behave inconsistently” is
making the RFC (and by extension) the language any more consistent. In
this following example snippet it's not at all obvious that the `break`
is behaving incorrectly by targeting the `using()` with `continue`
targeting the `foreach()`, despite both using the same “number”:
$processed = 0;
foreach ($entries as $entry) {
using ($db->transaction()) {
switch ($entry['type']) {
case 'EOF':
break 2;
default:
if (should_skip($entry)) {
continue 2;
}
$db->insert($entry);
}
}
$processed++;
}
I'm also noticing that the RFC still does not explain *why* the decision
for `break` to target `using()` has been made. For your reference, my
last email asking that question is this one:
https://news-web.php.net/php.internals/129771. I didn't receive a reply
to that email either (and neither do the list archives have a reply).
2. Several people (including us) were uncomfortable with using a
boolean return from the exitContext() method. While that is what
Python does, it is indeed not self-evident how it works. (Should true
mean "true, I'm done" or "true, rethrow"?) We debated using an enum
value, but that appeared to be too verbose.
Instead, we decided that exitContext() should return ?Throwable, which
is the same thing it is passed. In a success case, it is passed null.
In a failure case, it is passed a throwable. So it can then return
null (meaning "we're done, nothing else to do here") or a throwable,
which will then get thrown. Since in most cases an error should be
allowed to propagate, it means simply calling `return $exception` at
the end of the method will "do the right thing" 95% of the time.
Simple and easy and self-documenting. (If there's a reason to wrap and
rethrow the exception, do that and return the new exception. Or to
swallow the exception and not propagate it, return null.)
That sounds like a “throw” statement with extra steps [1]. While nothing
stopped you from writing `throw new SomeException();` within
`exitContext()` with the `bool` return value, it at least *encouraged*
you to not replace the original Exception with another Exception
entirely and to just make a decision between “suppress” or “not
suppress”. Now `return` is completely equivalent to `throw` (at least as
long as `exitContext()` doesn't contain a `catch()` itself) adding even
more layers of “using() is magically including behavior of other
language constructs” that will be hard to reason about for humans and
machines alike.
Best regards
Tim Düsterhus
[1]
function raise(\Throwable $e): ContextManager {
return new class ($e) implements ContextManager
{
public function __construct(private \Throwable $e) { }
public function enterContext(): mixed { }
public function exitContext(?\Throwable $e = null):
?Throwable {
return $this->e;
}
};
}
using(raise(new \Exception())) { }