Arnaud and I would like to present another RFC for consideration: Context Managers.
https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim and Seifeddine. Both proposals grew out of casual discussion several months ago; I don't believe either team was aware that the other was also actively working on such a proposal, so we now have two. C'est la vie. :-)
Naturally, Arnaud and I feel that our approach is the better one. In particular, as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters. It also does not allow differentiating between a success or failure exit condition, which for many use cases is absolutely mandatory (as shown in the examples in the context manager RFC).
The Context Manager proposal is a near direct port of Python's approach, which is generally very well thought-out. However, there are a few open questions as listed in the RFC that we are seeking feedback on.
Discuss. :-)
--
Larry Garfield
larry@garfieldtech.com
Hello.
Thank you for the RFC.
An excellent tool for a language that supports interfaces.
--
Ed
ср, 5 нояб. 2025 г., 09:42 Edmond Dantes edmond.ht@gmail.com:
Hello.
Thank you for the RFC.
An excellent tool for a language that supports interfaces.--
Ed
Hi, Larry!
Have you considered returning enum instead of ?bool? It would have a clear
self explanatory meaning.
—
Valentin
Hello all.
Have you considered returning enum instead of ?bool? It would have a clear self explanatory meaning.
You don’t need to return anything at all. :)
PHP already has throw.
That means the cleanup method can throw an exception if it decides one
should be thrown.
This behavior is fully consistent with PHP’s design, and there’s no
need for a return statement.
--
Ed
Arnaud and I would like to present another RFC for consideration: Context
Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim
and Seifeddine. Both proposals grew out of casual discussion several
months ago; I don't believe either team was aware that the other was also
actively working on such a proposal, so we now have two. C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is unreliable
if timing matters. It also does not allow differentiating between a
success or failure exit condition, which for many use cases is absolutely
mandatory (as shown in the examples in the context manager RFC).The Context Manager proposal is a near direct port of Python's approach,
which is generally very well thought-out. However, there are a few open
questions as listed in the RFC that we are seeking feedback on.Discuss. :-)
Great idea, I'm definitely behind this.
I've also read through all the PR code.
I have a few questions
-
Apart from wrapping zend_resource into ResourceContext, has there been
discussions or ideas to wrap other things ? -
Are there any scenarios where using with() is a bad idea or has "side
effects"? -
In the implementation code there is a lot of mention of "list" and
zend_list .. why? Maybe the answer is obvious but I can't see, at first
glance, why we are implementing list under the hood.
Thanks, and great work to both of you!
--
Larry Garfield
larry@garfieldtech.com
Hi Paul,
- In the implementation code there is a lot of mention of "list" and zend_list .. why? Maybe the answer is obvious but I can't see, at first glance, why we are implementing list under the hood.
zend_list.h is the API for managing resources in core.
Best Regards,
Arnaud
Hi Paul,
- In the implementation code there is a lot of mention of "list" and
zend_list .. why? Maybe the answer is obvious but I can't see, at first
glance, why we are implementing list under the hood.
zend_list.his the API for managing resources in core.
Hi Arnaud,
Ok that answers it, thanks.
Best Regards,
Arnaud
Arnaud and I would like to present another RFC for consideration: Context
Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim
and Seifeddine. Both proposals grew out of casual discussion several
months ago; I don't believe either team was aware that the other was also
actively working on such a proposal, so we now have two. C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is unreliable
if timing matters. It also does not allow differentiating between a
success or failure exit condition, which for many use cases is absolutely
mandatory (as shown in the examples in the context manager RFC).The Context Manager proposal is a near direct port of Python's approach,
which is generally very well thought-out. However, there are a few open
questions as listed in the RFC that we are seeking feedback on.Discuss. :-)
--
Larry Garfield
larry@garfieldtech.com
Great RFC and I really like how much more readable the code can become with
this approach.
Out of curiosity, what happens if GOTO is used inside a context block to
jump away from it?
Could the RFC clarify the relation between Context and switch/case? I
thought it was really odd that something that triggers a warning on
switch/case is being introduced into a brand new language construct
basically creating the possibility for new code to fall into the same trap
as opposed to avoiding it in the first place. Specially a construct like
switch/case that has been in decline for over a decade and ever since match
came out on 8.0, switch case is practically deprecated without actually
being deprecated yet. What’s the importance/relevance of being consistent
with it?
While we’re at it, do we really need break; statements inside context
blocks? If you want out you can:
- return
- throw
In the case of a nested block (break 2;) where I don’t want to wrap the
entire thing in try/catch, it seems like a GOTO out of it would be more
meaningful with text-based identifiers rather than number-based, which
leads to my first question (although I was more curious than actually
making an argument for it because I would rather avoid nested with as much
as possible).
Marco Deleu
Out of curiosity, what happens if GOTO is used inside a context block
to jump away from it?
I don't think this is crazy enough. I'm curious what is supposed to
happen if you goto into one!
Btw is the naming clash with global functions real? I've seen some
with() helpers here or there but you can't use a function in a
with($something) {} and you can't use the new control structure as a
callable, so where's the ambiguity requiring to make the keywordd
reserved?
BR,
Juris
Arnaud and I would like to present another RFC for consideration: Context Managers.
https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim and Seifeddine. Both proposals grew out of casual discussion several months ago; I don't believe either team was aware that the other was also actively working on such a proposal, so we now have two. C'est la vie. :-)
Naturally, Arnaud and I feel that our approach is the better one. In particular, as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters. It also does not allow differentiating between a success or failure exit condition, which for many use cases is absolutely mandatory (as shown in the examples in the context manager RFC).
The Context Manager proposal is a near direct port of Python's approach, which is generally very well thought-out. However, there are a few open questions as listed in the RFC that we are seeking feedback on.
Discuss. :-)
Larry, Arnaud,
I really like this RFC but have a couple of things to discuss:
-
automatically re-throwing exceptions: I think that this behavior, especially with a boolean return value deciding if it happens or not is not intuitive. I think a better approach is to do nothing with the exception and let the user re-throw it if desired. I can't think of anywhere else we re-throw exceptions unless the user indicates otherwise. I'd rather leave the return value for return values; we could expand this allow access to the return value like: with (foo() as $foo return $bar) { }, and $bar would be set to null on void returns.
-
context variable and scope: I know that you explicitly are not creating a new scope, this means that the context variable will clash with the enclosing scope namespace, and then the variable will be unset after the context ends, this doesn't sit so well with me. I think I'd rather see the same behavior as arrow function arguments, where it does not override variables of the same name in the enclosing scope and whatever value it has is lost at the end of the context, leaving the outer scope version intact.
At worst though, I'm sure IDE and static analyzers will be able to detect the "use after unset" behavior with clashing variable names, causing the developer to resolve it, and it'll be fine either way.
Thanks for the great RFC!
- Davey
Hi
Is the keyword "with" reserved, semi-reserved, ... ?
Asking because I've seen global "with" functions before, like in Laravel for example.
What really is a footgun of the resource type is that they can represent an illegal state.
This proposal leans into having close functions (or equivalent) for resources/objects that make it possible to represent such illegal state; something I'm fundamentally against.
As an aside, under "Rejected Features", there's a false statement:
Destructors are not always called immediately when the refcount hits zero.
Kind regards
Niels
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
Hi all. I'm going to reply to several people at once in a single message, for simplicity:
- Apart from wrapping zend_resource into ResourceContext, has there
been discussions or ideas to wrap other things ?
Not really. The intent is that users can create their own "setup and teardown" logic packages and do with them as they please. Resources are just an oddball case in PHP, because reasons. I'm not sure what other auto-wrapping cases would make sense.
That said, PHP could absolutely ship context managers for people to use explicitly. My ideal way to address async would be exactly that: The Scope example from the RFC, where the only way to get to a scope (and therefore start coroutines) is via a context manager, and PHP iself provides the scope types we want to support. Nothing else.
There may be other managers that PHP would want to ship in the future for whatever reason, but that's out of scope for now. (No pun intended.)
- Are there any scenarios where using with() is a bad idea or has
"side effects"?
If you have a setup/teardown routine that is only used once or twice, then making a context manager for just that one use case is likely overkill. Just write try/catch/finally as normal.
I don't believe a context manager could handle this, although I've only rarely seen it in the wild:
$success = true;
try {
// ...
}
catch (\Exception $e) {
$success = false;
}
if ($success) { ... }
(That is, leaking a variable from the setup/teardown code into the surrounding scope. Though, I suppose this would work in a pinch:
$success = false;
with (new Foo() as $f) {
// ...
$success = true;
}
if ($success) { ... }
Not ideal, but would work.
We're still exploring the possibility of making the context manager keyword an expression rather than a statement, which might offer other alternatives. Still brainstorming.
- In the implementation code there is a lot of mention of "list" and
zend_list .. why? Maybe the answer is obvious but I can't see, at first
glance, why we are implementing list under the hood.
I will defer to Arnaud here.
Have you considered returning enum instead of ?bool? It would have a
clear self explanatory meaning.
We have discussed that a bit, actually. The main concern is usability. The typical case will be to allow exceptions to propagate. If, say, a TypeError gets thrown 4 function calls down, you probably do want that to propagate to your top level handler, but still want to rollback your transaction or close your file or whatever. So the typical case should be easy, hence why we said null means the default behavior. And since null is falsy`, that fits neatly into a ?bool return; that is also what Python uses.
With an enum, you'd have a much longer thing to type, plus it's less self-evident what no-return means. Ie, you'd have:
function exitContext(?Exception $e): ContextResult
{
if ($e) {
$this->conn->rollback();
// This line becomes required.
return ContextResult::Propagate;
}
$this->conn->commit();
return ContextResult::Done; // Or, eh, what?
}
And not returning becomes a type error.
Alternatively, we could assume null implies one of the other cases; but as shown above, there's still the issue that the return type is only meaningful in case of an exception, so it's unclear how that interacts.
We're still open to discussion here. It also would play into the outstanding question of with as an expression.
Out of curiosity, what happens if GOTO is used inside a context block
to jump away from it?
That would be a success case, just like break or return. Basically anything other than an exception is a success case. (That said, please don't use Goto. :-) )
Could the RFC clarify the relation between Context and switch/case? I
thought it was really odd that something that triggers a warning on
switch/case is being introduced into a brand new language construct
basically creating the possibility for new code to fall into the same
trap as opposed to avoiding it in the first place. Specially a
construct like switch/case that has been in decline for over a decade
and ever since match came out on 8.0, switch case is practically
deprecated without actually being deprecated yet. What’s the
importance/relevance of being consistent with it?
break and continue are interesting keywords. (In the "may you live in interesting times" sense.) Sometimes they have the same effect, if a control structure is non-looping. Or they may have different effects in case it is. The main non-looping case is switch, where for reasons that were before my time the decision was made to deprecate continue in favor of just supporting break. However, blocking it entirely is a problem, because that would change where continue 2 would go (as switch would be removed as a "level" that it could go to). It is kind of a mess.
with is a non-looping control structure, and thus it seems logical to be consistent with other non-looping control structures. But, as noted, the other non-looping control structure is a mess. :-) Therefore, we get to choose between "a consistent mess" and "an inconsistent non-mess in one place and a mess in another."
Neither is a fantastic option. We're open to both, depending on what the consensus is.
While we’re at it, do we really need break; statements inside context
blocks? If you want out you can:
- return
- throw
Both of those exit the function the with statement is in, which is not always desireable.
In the case of a nested block (break 2;) where I don’t want to wrap the
entire thing in try/catch, it seems like a GOTO out of it would be more
meaningful with text-based identifiers rather than number-based, which
leads to my first question (although I was more curious than actually
making an argument for it because I would rather avoid nested with as
much as possible).
Because Goto was added to PHP as a troll, and not a feature you should actually use in production code 99.999% of the time. :-)
I really like this RFC but have a couple of things to discuss:
- automatically re-throwing exceptions: I think that this behavior,
especially with a boolean return value deciding if it happens or not is
not intuitive. I think a better approach is to do nothing with the
exception and let the user re-throw it if desired. I can't think of
anywhere else we re-throw exceptions unless the user indicates
otherwise. I'd rather leave the return value for return values; we
could expand this allow access to the return value like: with (foo() as
$foo return $bar) { }, and $bar would be set to null on void returns.
Using the return value of exitContext() as the result of a "with expression" is something we are considering.
However, we're modeling on Python (the most robust such functionality we are aware of), and they rethrow by default. Essentially, the concept is that exitContext() (and it's Python equivalent magic method), is mostly a finally block, not a catch block. finally blocks do propagate exceptions. In practice, many exitContext() methods will not need to differentiate; they'll just close a file or whatever and move on with life, which is why you would want an exception to propagate. The inclusion of the exception parameter makes it a sort of combined catch/finally, so it has some behavior of each.
Another option we kicked around was splitting it into two methods; catchContext(Throwable $e) and exitContext(). However, that creates two other problems:
-
Because it's an interface, you would need to implement catchContext() all the time, even if you don't need it. That's very inconvenient. (Shamless plug for revisiting Levi's Interface Default Methods RFC, which would solve this issue: https://wiki.php.net/rfc/interface-default-methods) Using magic methods instead would avoid that problem, but then we're dealing with magic methods rather than a clearly-detectable interface.
-
If you need to run logic in both methods, do you duplicate it? Or worse, if you have logic that runs only on a success case, then what? Most likely you'd need to have your own $wasItAnError property inside the context manager object, which is ugly and annoying.
That said, we're open to other ways to structure this logic. But I think in practice it's true that most use cases will want to propagate the exception, after doing appropriate local cleanup.
- context variable and scope: I know that you explicitly are not
creating a new scope, this means that the context variable will clash
with the enclosing scope namespace, and then the variable will be unset
after the context ends, this doesn't sit so well with me. I think I'd
rather see the same behavior as arrow function arguments, where it does
not override variables of the same name in the enclosing scope and
whatever value it has is lost at the end of the context, leaving the
outer scope version intact.
Arnaud says that masking the context variable itself is probably fairly straightforward, so we can go ahead and do that. However, masking every variable that gets created doesn't make sense. This construct is not creating a new "block scope" in the language. It's just desugaring into a reusable try-catch-finally construct.
If we wanted to have an actual local scope specific to the with block, then instead of the statement list we should have a callable, which in most cases would be an anon function. However, PHP's anon functions suck to use because of the need to explicitly use variables. That would effectively eliminate any benefit this feature offers, because you can already do $someWrapper->do($aCallable). But $aCallable needs a long list of use statements, which makes it fugly.
If anon functions were fixed, that would make that approach easier to do. However, that's been tried at least twice and it's been shot down both times, so I'm assuming we're stuck with a clunky anon function syntax indefinitely.
Also, off-list discussion has shown an interest in multiple context managers in one with block, which was one of the outstanding open questions. It looks like we'll probably include that, as it should be easy enough to do.
And now the big one... also in off-list discussion, Seifeddine noted that Laravel already defines a global function named with: https://github.com/laravel/framework/blob/12.x/src/Illuminate/Support/helpers.php#L510
And since this RFC would require with to be a semi-reserved keyword at the parser/token level, that creates a conflict. (This would be true even if it was namespaced, although Laravel is definitely Doing It Wrong(tm) by using an unnamespaced function.) Rendering all Laravel deployments incompatible with PHP 8.6 until it makes a breaking API change would be... not good for the ecosystem.
So that means using Python's with keyword here is not going to work. Damn.
A couple of other options have presented themselves, but we're open to other suggestions, too:
- Java uses a parenthetical block on
tryfor similar functionality (though without a separate context manager). That would look like:
try (new Foo() as $foo) {
// ...
}
// catch and finally become optional if there is a context.
Pros here is that it introduces no new keywords, and context managers are effectively "packaged try-catch-finally" logic, so it fits. Downsides are that it gets more confusing now that try only sometimes requires a catch or finally. The ordering between the context manager and explicit catch/finally blocks is also non-obvious. It would also entirely preclude context blocks being an expression, as try is already non-expressional.
- Either
useorusing. The semantics here would be identical to the currentwithproposal.
Pros here are that use is already a reserved word, and using is, I hope, still available in practice. They could also be implemented as expressions if we figure out a way to do so. Downsides are that use is already used in a bunch of places to mean different things, so adding yet another contextual meaning just increases the complexity/confusion. using wouldn't have that issue, but we would still need to verify if it's available.
--Larry Garfield
breakandcontinueare interesting keywords. (In the "may you live in interesting times" sense.) Sometimes they have the same effect, if a control structure is non-looping. Or they may have different effects in case it is. The main non-looping case isswitch, where for reasons that were before my time the decision was made to deprecatecontinuein favor of just supportingbreak. However, blocking it entirely is a problem, because that would change wherecontinue 2would go (asswitchwould be removed as a "level" that it could go to). It is kind of a mess.
Nikita's original proposal was indeed to ban continue targeting switch:
https://wiki.php.net/rfc/continue_on_switch_deprecation
That doesn't mean any targets would get re-numbered, it just means that
the case which currently raises a Warning would have thrown an Error.
It was talked down to a Warning during discussion:
https://externals.io/message/102393 That was partly about backwards
compatibility, which doesn't apply here, so I personally think either
Warning or Error would be fine.
--
Rowan Tommins
[IMSoP]
Out of curiosity, what happens if GOTO is used inside a context
block to jump away from it?That would be a success case, just like break or return. Basically
anything other than an exception is a success case. (That said,
please don't use Goto. :-) )
I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.
And now the big one... also in off-list discussion, Seifeddine noted
that Laravel already defines a global function namedwith:
https://github.com/laravel/framework/blob/12.x/src/Illuminate/Support/helpers.php#L510And since this RFC would require
withto be a semi-reserved keyword
at the parser/token level, that creates a conflict. (This would be
true even if it was namespaced, although Laravel is definitely Doing
It Wrong(tm) by using an unnamespaced function.) Rendering all
Laravel deployments incompatible with PHP 8.6 until it makes a
breaking API change would be... not good for the ecosystem.
PHP owns the top level namespace. That's been the going for as long as I
can remember. It was unwise for Laravel to flaunt that rule.
- Java uses a parenthetical block on
tryfor similar functionality
(though without a separate context manager). That would look like:try (new Foo() as $foo) {
// ...
}
// catch and finally become optional if there is a context.
…
- Either
useorusing. The semantics here would be identical to
the currentwithproposal.
IMO, the try syntax is more confusing than another overload of use.
cheers,
Derick
Out of curiosity, what happens if GOTO is used inside a context
block to jump away from it?That would be a success case, just like break or return. Basically
anything other than an exception is a success case. (That said,
please don't use Goto. :-) )I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.
I defer to Arnaud here.
And now the big one... also in off-list discussion, Seifeddine noted
that Laravel already defines a global function namedwith:
https://github.com/laravel/framework/blob/12.x/src/Illuminate/Support/helpers.php#L510And since this RFC would require
withto be a semi-reserved keyword
at the parser/token level, that creates a conflict. (This would be
true even if it was namespaced, although Laravel is definitely Doing
It Wrong(tm) by using an unnamespaced function.) Rendering all
Laravel deployments incompatible with PHP 8.6 until it makes a
breaking API change would be... not good for the ecosystem.PHP owns the top level namespace. That's been the going for as long as I
can remember. It was unwise for Laravel to flaunt that rule.
Yes, Laravel is in the wrong here, but AIUI even a namespaced function would conflict with a soft-reserved keyword. And regardless, breaking Laravel is not a great plan.
- Java uses a parenthetical block on
tryfor similar functionality
(though without a separate context manager). That would look like:try (new Foo() as $foo) {
// ...
}
// catch and finally become optional if there is a context.…
- Either
useorusing. The semantics here would be identical to
the currentwithproposal.IMO, the try syntax is more confusing than another overload of use.
cheers,
Derick
How about using?
--Larry Garfield
Hi Derick,
Out of curiosity, what happens if GOTO is used inside a context
block to jump away from it?That would be a success case, just like break or return. Basically
anything other than an exception is a success case. (That said,
please don't use Goto. :-) )I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.
In this case, goto is supported out of the box as anything necessary
to cleanup after with() is emitted in a finally block. So goto will
execute this before jumping to the actual target.
Best Regards,
Arnaud
Hey Larry, Tim, Seifeddine and Arnauld,
Arnaud and I would like to present another RFC for consideration: Context Managers.
https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim and Seifeddine. Both proposals grew out of casual discussion several months ago; I don't believe either team was aware that the other was also actively working on such a proposal, so we now have two. C'est la vie. :-)
Naturally, Arnaud and I feel that our approach is the better one. In particular, as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters. It also does not allow differentiating between a success or failure exit condition, which for many use cases is absolutely mandatory (as shown in the examples in the context manager RFC).
The Context Manager proposal is a near direct port of Python's approach, which is generally very well thought-out. However, there are a few open questions as listed in the RFC that we are seeking feedback on.
Discuss. :-)
I've been looking at both RFCs and I don't think either RFC is good
enough yet.
As for this RFC:
It makes it very easy to not call the exitContext() method when calling
enterContext() manually. The language (obviously) doesn't prevent
calling enterContext() - and that's a good thing. But also, it will not
enforce that exitContext() gets ever called (and it also cannot,
realistically).
Thus, we have a big pitfall, wherein APIs may expect enterContext() and
exitContext() to be called in conjunction, but users don't - with
possibly non-trivial side-effects (locks not cleared, transactions not
committed etc.). Thus, to be safe, implementers of the interface will
also likely need the destructor to forward calls to exitContext() as
well. But it's an easy thing to forget - after all, the intended usage
of the API just works. Why would I think of that, as an implementer of
the interface, before someone complains?
Ultimately you definitely will need the capability of calling
enterContext() and exitContext() manually too (i.e. restricting that is
not realistic either), as lifetimes do not necessarily cleanly nest - as
a trivial example, you might want to obtain access to a handle which is
behind a lock. You'll have to enter the context of the lock, enter the
context of the handle, and close the lock (because more things are
locked behind that lock, including the handle). ... But you don't
necessarily want the hold on the lock to outlive the inner handle. In
short: The proposed approach only allows nesting contexts, but not
interleaving them.
Further, calling with() twice on an object is quite bad in general. But
it might easily happen - you have a function which wants a transaction.
e.g. function writeSomeData(DatabaseTransaction $t) { with ($t) {
$t->query("..."); } }. A naive caller might think, DatabaseTransaction
implements ContextManager ... so let's wrap it: with($db->transaction()
as $t) { writeSomeData($t); }. But now you are nesting a transaction,
which may have unexpected side effects - and the code probably not
prepared to handle it. So, you have to add yet another safeguard into
your implementation: check whether enterContext() is only active once.
... Or, maybe a caller assumes that $t = $db->transaction(); with ($t) {
$t->query("..."); } with ($t) { $t->query("..."); } is fine - but the
implementation is not equipped to handle multiple calls to enterContext().
Additionally, I would expect implementers to want to provide methods,
which can be called while the context is active. However, it's not
impossible to call these methods without wrapping it into with() or
calling the enterContext() method explicitly. One more failure mode,
which needs handling.
Like for example, calling $t->query() on a transaction without starting it.
I don't like that design, which effectively forces you to put safety
checks for all but the simplest cases onto the ContextManager
implementation.
And it forces the user to recognize "this returned object
DatabaseTranscation actually implements ContextManager, thus I should
put it into with() and not immediately call methods on it". (A problem
which the use() proposal from Tim does not have by design.)
The choice of adding the exception to the exitContext() is interesting,
but also very opinionated:
- It means, that the only way to abort, in non-exceptional cases, is to
throw yourself an exception. And put a try/catch around the with() {}
block. Or manually use enterContext() & exitContext() - with a fake "new
Exception" essentially. - Maybe you want to hold a transaction, but just ensure that everything
gets executed together (i.e. atomicity), but not care about whether
everything actually went through (i.e. not force a rollback on
exception). You'll now have to catch the exception, store it to a
variable, use break and check for the exception after the with block.
Or, yes, manually using enterContext() and exitContext().
It feels like with() is designed to be covering 70% of the use cases,
with a load of hidden pitfalls and advanced usage requiring manual
enterContext() and exitContext() calls. It's not a very good solution.
As to the destructors (and also in reply to that other email from
Arnauld talking about PDO::disconnect() etc.):
It's already possible today to have live objects which are already
destructed. It's extremely common to have in shutdown code. It's
sometimes a pain, I agree. But it's an already known pain, and an
already handled pain in a lot of code.
If your object only knows "create" and "destruct", there's no way for a
double enterContext() (nested or consecutive) situation to ever happen.
(Well, yes, you could theoretically manually call __destruct(), but
why would you ever do that?)
Last thing - proper API usage forces you to use that construct.
To the use() proposal from Tim:
This proposal makes it very simple to inadvertently leak the use()'d
value. I don't think the current proposed form goes far enough.
However we could decide to force-destruct an object (just like we do in
shutdown too). It's just one single flag for child methods to check as
well - the object is either destructed or not. We could also trivially
prohibit nested use() calls by throwing an AlreadyDestructedError when
an use()'d and inside destructed object crosses the use() boundary.
The only disadvantage is that there's no information about thrown
exceptions. I.e. you cannot add a default behaviour of "on exception,
please do this", like rolling transactions back. But:
- Is it actually a big problem? Where is the specific disadvantage over
simply $db->transaction(function($t) { /* do stuff */ }); - where the
call of the passed Closure can be trivially wrapped in try/catch. - If yes, can we e.g. add an interface ExceptionDestructed { public bool
$destructedDuringException; }? Which will set that property if the
property is still undefined - to true if the destructor gets triggered
inside ZEND_HANDLE_EXCEPTION. To false otherwise. And, if an user
desires to manually force success/failure handling, he may set
$object->destructedDuringException = true; himself as a very simple -
one-liner - escape hatch.
The use() proposal is not a bad one, but I feel like requiring the RC to
drop to zero first, misses a bit of potential to save users from mistakes.
The other nice thing about use() is that it's optional. You don't have
to use it. You use it if you want some scoping, otherwise the scope is
simply the function scope.
To both proposals:
It remains possible by default to call methods on the object, after
leaving the with or use block. So some checking on methods for a safe
API is probably still required.
I don't think it's possible to solve that problem at all with the
ContextManager RFC, except manual checking by the implementer in every
single method. But it's possibly possible to solve it with the use() RFC
in orthogonal ways - like a #[ProhibitDestructed] attribute, which can
be added onto a class (making it apply to all methods) or a specific
method and causes method calls to throw an exception when the object is
destructed.
Which is possible to provide by the language, as the language knows
about whether objects are already destructed, unlike e.g. the
ContextManager, where it would be object state, which has to be
maintained by the user.
TL;DR: ContextManagers are a buttload of pitfalls. use() is probably
better, with much less inherent problems. And with the remaining
problems of the proposal being actually solvable.
Thanks,
Bob
I don't like that design, which effectively forces you to put safety
checks for all but the simplest cases onto the ContextManager
implementation.
And it forces the user to recognize "this returned object
DatabaseTranscation actually implements ContextManager, thus I should
put it into with() and not immediately call methods on it". (A problem
which the use() proposal from Tim does not have by design.)
I think you may have missed the key distinction between a "Context
Manager" (as designed by Python) and a "Disposable" (as used in C# and
others): the Context Manager is not the resource itself, it exists only
to meet the protocol/interface.
In this code:
with ( $dbConnection->transaction() as $handle ) {
$handle->execute('I am in the transaction');
}
$handle is not the value returned by $dbConnection->transaction(),
it's the value returned by $dbConnection->transaction()->enterContext().
One of the things that means is that if you just write
$foo=$dbConnection->transaction() you can't accidentally run any methods
on $foo, if all it has is enterContext and exitContext.
It also means you can trivially wrap values that have no idea about
context managers, without needing a load of extra proxy code; it's why
the RFC can include implicit handling for resources; and why the
generator example in the Future Scope section works.
--
Rowan Tommins
[IMSoP]
Hey Rowan,
I don't like that design, which effectively forces you to put safety
checks for all but the simplest cases onto the ContextManager
implementation.
And it forces the user to recognize "this returned object
DatabaseTranscation actually implements ContextManager, thus I should
put it into with() and not immediately call methods on it". (A
problem which the use() proposal from Tim does not have by design.)I think you may have missed the key distinction between a "Context
Manager" (as designed by Python) and a "Disposable" (as used in C# and
others): the Context Manager is not the resource itself, it exists
only to meet the protocol/interface.In this code:
with ( $dbConnection->transaction() as $handle ) {
$handle->execute('I am in the transaction');
}$handle is not the value returned by $dbConnection->transaction(),
it's the value returned by $dbConnection->transaction()->enterContext().One of the things that means is that if you just write
$foo=$dbConnection->transaction() you can't accidentally run any
methods on $foo, if all it has is enterContext and exitContext.
You are right, I missed that there's an extra layer of nesting inside this.
I think the DatabaseTransaction example put me on the wrong thought path
because it just returned the connection it came from instead of a
dedicated nested Transaction object. (The enterContext method in that
example ought to include a startTransaction call.)
However, I still think the proposed approach is dangerous with respect
to forgetting the exitContext() call. When using manual handling. But
yes, I agree, that's a much more manageable concern.
And the onus of handling duplicate enterContext() and multiple
exitContext() calls still lies on the implementer. The RFC does zero
effort at addressing this.
Thanks,
Bob
However, I still think the proposed approach is dangerous with respect to forgetting the exitContext() call. When using manual handling. But yes, I agree, that's a much more manageable concern.
And the onus of handling duplicate enterContext() and multiple exitContext() calls still lies on the implementer. The RFC does zero effort at addressing this.
I think the relationship to Iterators is significant: if you put an iterator in a variable, it's perfectly possible to use it in two different foreach statements, or manually call the interface methods, and get very confusing results.
But most of the time, you don't take a reference to the iterator at all, and the same would be true of Context Managers:
foreach ( $foo->iterate() as $item ) { ... }
with ( $foo->guard() as $resource ) { ... }
That said, it seems like it would be easy enough to add a mandatory state check - a boolean property on the interface, and a check in the de-sugared code:
if ( ! $__mgr->canEnter ) { throw SomeError; }
$__mgr->canEnter = false;
That would also give the implementation a choice of whether to reset it on exit, or make the object strictly single use.
That doesn't stop you manually calling the enterContext and exitContext methods in unintended ways, but that's actually true of RAII or Disposable designs, at least in PHP: there's nothing stopping you calling __construct or __destruct as normal methods, and causing all sorts of unintended behaviour.
Rowan Tommins
[IMSoP]
Hi
Am 2025-11-06 13:18, schrieb Rowan Tommins [IMSoP]:
I think the relationship to Iterators is significant: if you put an
iterator in a variable, it's perfectly possible to use it in two
different foreach statements, or manually call the interface methods,
and get very confusing results.But most of the time, you don't take a reference to the iterator at
all, and the same would be true of Context Managers:foreach ( $foo->iterate() as $item ) { ... }
with ( $foo->guard() as $resource ) { ... }
The reference to the sibling thread probably fits even better here than
in my previous reply. So I'm putting another reference:
https://news-web.php.net/php.internals/129467
there's nothing stopping you calling __construct or __destruct as
normal methods, and causing all sorts of unintended behaviour.
That is technically true and there are indeed some “expert-level” use
cases where calling these manually - except for parent:: I guess - is
required (e.g. lazy objects), but there's a very significant difference:
Those are magic methods following the naming scheme of magic methods.
The __ prefix is a visual indicator that the methods are somehow
special and probably not meant to be used directly.
The documentation at
https://www.php.net/manual/en/language.oop5.magic.php states
(highlighting mine):
Magic methods are special methods which override PHP's default's
action when certain actions are performed on an object.All methods names starting with __ are reserved by PHP.
This is not true for the context manager methods. From a developer's PoV
these are normal methods on a class with nothing screaming “be careful”.
The interface could serve as a invitation for folks to read the
documentation on how the interface methods are meant to be used, but we
all know how that works in practice with an IDE suggesting
normal-looking methods that appear to do the right thing (until they
don't).
Best regards
Tim Düsterhus
You are right, I missed that there's an extra layer of nesting inside this.
I think the DatabaseTransaction example put me on the wrong thought path
because it just returned the connection it came from instead of a
dedicated nested Transaction object. (The enterContext method in that
example ought to include a startTransaction call.)However, I still think the proposed approach is dangerous with respect
to forgetting the exitContext() call. When using manual handling. But
yes, I agree, that's a much more manageable concern.
And the onus of handling duplicate enterContext() and multiple
exitContext() calls still lies on the implementer. The RFC does zero
effort at addressing this.Thanks,
Bob
Bob, you seem to be focused on the "manual call" case. That... is not a case that exists. I cannot think of any situation where a user creates a context manager and then calls enter/exit context themselves that isn't a "stop that, you're doing it wrong" situation.
By the same token, you can implement Iterator and then call current() and next() and valid() yourself... but odds are you're doing something wrong, and it's very easy to screw things up if you call those methods in the wrong order or too many times or whatever. If you implement Iterator, you're supposed to use it with foreach(). Doing anything else with it is "well technically that maybe works, but please don't."
As Rowan noted, calling __construct() or __destruct() yourself is also possible, and can cause things to go sideways, and if they do then it's your own damned fault, don't do that.
The same is true here. Someone manually calling enterContext and then not calling exitContext() is... simply not a use case that matters, because there's no good reason to ever do so. The only effort the RFC needs to make in this regard is say "don't do that."
--Larry Garfield
Hi
Am 2025-11-09 16:07, schrieb Larry Garfield:
Bob, you seem to be focused on the "manual call" case. That... is not
a case that exists. I cannot think of any situation where a user
creates a context manager and then calls enter/exit context themselves
that isn't a "stop that, you're doing it wrong" situation.
See my sibling reply to Rowan
(https://news-web.php.net/php.internals/129468).
By the same token, you can implement Iterator and then call
current()
andnext()and valid() yourself... but odds are you're doing something
wrong, and it's very easy to screw things up if you call those methods
in the wrong order or too many times or whatever. If you implement
Iterator, you're supposed to use it with foreach(). Doing anything
else with it is "well technically that maybe works, but please don't."
You are wrong here. Manually interacting with an Iterator is not just
safe, it is also necessary for some use cases. The simplest example
would be iterating two Iterators in lock-step (i.e. doing a zip()
operation / array_combine()).
As Rowan noted, calling __construct() or __destruct() yourself is also
possible, and can cause things to go sideways, and if they do then it's
your own damned fault, don't do that.The same is true here. Someone manually calling enterContext and then
not calling exitContext() is... simply not a use case that matters,
because there's no good reason to ever do so. The only effort the RFC
needs to make in this regard is say "don't do that."
See sibling reply for an explanation how __construct() is different.
Best regards
Tim Düsterhus
You are wrong here. Manually interacting with an Iterator is not just
safe, it is also necessary for some use cases. The simplest example
would be iterating two Iterators in lock-step (i.e. doing azip()
operation /array_combine()).
It is certainly possible to use the Iterator methods safely without a
foreach() construct or built-in aggregate; but it's also easy to miss
the implications, and cause very confusing behaviour. For instance, you
could call next() in two different pieces of code, and each would miss
half the items; or you could call rewind() on an iterator that was
already in use elsewhere.
The same is true of the ContextManager interface: you could certainly
call enterContext() and exitContext() manually with no problems at all.
It might even be necessary, for much the same reasons as iterators: you
might want to write code that aggregates two context managers in a
specific arrangement.
As I understand it, your concern is that someone will call
enterContext() without exitContext(). You could do exactly the same
thing if you manually handle an Iterator.
Consider this method:
function getDataIterator(): Generator {
$this->acquireLock();
while ( $this->hasMoreResults() ) {
yield $this->getNextResult();
}
$this->releaseLock();
}
If the user calls $it = $foo->getDataIterator(), they can call
$it->current() and get a result, but never call $it->next(). They could
even use it in a foreach() loop but break out before consuming all the
results. In such cases, the lock would never be released, unless there's
an extra safety check in the destructor.
I think the currently proposed names actually make that less likely
for a ContextManager: if you enter something, you probably want to exit
it later.
We could reinforce that further by also prefixing the method names with
"__", but we don't have any precedent for that, and misuse would still
be possible.
--
Rowan Tommins
[IMSoP]
You are wrong here. Manually interacting with an Iterator is not just
safe, it is also necessary for some use cases. The simplest example
would be iterating two Iterators in lock-step (i.e. doing azip()
operation /array_combine()).It is certainly possible to use the Iterator methods safely without a
foreach() construct or built-in aggregate; but it's also easy to miss
the implications, and cause very confusing behaviour. For instance, you
could callnext()in two different pieces of code, and each would miss
half the items; or you could callrewind()on an iterator that was
already in use elsewhere.The same is true of the ContextManager interface: you could certainly
call enterContext() and exitContext() manually with no problems at all.
It might even be necessary, for much the same reasons as iterators: you
might want to write code that aggregates two context managers in a
specific arrangement.As I understand it, your concern is that someone will call
enterContext() without exitContext(). You could do exactly the same
thing if you manually handle an Iterator.Consider this method:
function getDataIterator(): Generator {
$this->acquireLock();
while ( $this->hasMoreResults() ) {
yield $this->getNextResult();
}
$this->releaseLock();
}If the user calls $it = $foo->getDataIterator(), they can call
$it->current() and get a result, but never call $it->next(). They could
even use it in a foreach() loop butbreakout before consuming all the
results. In such cases, the lock would never be released, unless there's
an extra safety check in the destructor.
Excellent example, thank you.
I think the currently proposed names actually make that less likely
for a ContextManager: if you enter something, you probably want to exit
it later.We could reinforce that further by also prefixing the method names with
"__", but we don't have any precedent for that, and misuse would still
be possible.
Python uses magic methods here, and we considered it, but given that you will basically always want both methods we felt an interface was easier and provided better type checks. If there were multiple callbacks (say, a different one for a success exit and a fail exit), then magic methods might make more sense.
If you're suggesting having interface methods that begin with __, I agree that would be novel for no particular value. You can't screw up ContextManagers through misuse any more than you can screw up Iterator or ArrayAccess, and we've survived just fine with those for literally decades.
--Larry Garfield
One of the things that means is that if you just write
$foo=$dbConnection->transaction() you can't accidentally run any methods
on $foo, if all it has is enterContext and exitContext.
Since I first started following these two proposals, I've been wondering what happens if commit returns a failure code, such as when deferred constraints are used and a constraint fails.
-Jeff
Hi
Am 2025-11-06 00:27, schrieb Rowan Tommins [IMSoP]:
I think you may have missed the key distinction between a "Context
Manager" (as designed by Python) and a "Disposable" (as used in C# and
others): the Context Manager is not the resource itself, it exists only
to meet the protocol/interface.In this code:
with ( $dbConnection->transaction() as $handle ) {
$handle->execute('I am in the transaction');
}$handle is not the value returned by $dbConnection->transaction(),
it's the value returned
by $dbConnection->transaction()->enterContext().
For reference: This topic was/is also discussed in your sibling thread
starting at https://news-web.php.net/php.internals/129464. I also
stumbled upon this being completely unexpected from just looking at the
code. If multiple participants on this list with a above-average
knowledge of PHP (or even engine knowledge) find this unclear, I am
really concerned about the clarity for “regular PHP developers”.
Best regards
Tim Düsterhus
The choice of adding the exception to the exitContext() is
interesting, but also very opinionated:
- It means, that the only way to abort, in non-exceptional cases, is
to throw yourself an exception. And put a try/catch around the with()
{} block. Or manually use enterContext() & exitContext() - with a fake
"new Exception" essentially.- Maybe you want to hold a transaction, but just ensure that
everything gets executed together (i.e. atomicity), but not care about
whether everything actually went through (i.e. not force a rollback on
exception). You'll now have to catch the exception, store it to a
variable, use break and check for the exception after the with block.
Or, yes, manually using enterContext() and exitContext().
The Context Manager is given knowledge of the exception, but it's not
obliged to change its behaviour based on that knowledge. I don't think
that makes the interface opinionated, it makes it extremely flexible.
It means you can write this, which is impossible in a destructor:
function exitContext(?Throwable $exception) {
if ( $exception === null ) {
$this->commit();
} else {
$this->rollback();
}
}
But you could also write any of these, which are exactly the same as
they would be in __destruct():
// Rollback unless explicitly committed
function exitContext(?Throwable $exception) {
if ( ! $this->isCommitted ) {
$this->rollback();
}
}
// Expect explicit commit or rollback, but roll back as a safety net
function exitContext(?Throwable $exception) {
if ( ! $this->isCommitted && ! $this->isRolledBack ) {
$this->logger->warn('Transaction went out of scope without
explicit rollback, rolling back now.');
$this->rollback();
}
}
// User can choose at any time which action will be taken on destruct / exit
function exitContext(?Throwable $exception) {
if ( $this->shouldCommitOnExit ) {
$this->commit();
} else {
$this->rollback();
}
}
You could also combine different approaches, using the exception as an
extra signal only if the user hasn't chosen explicitly:
function exitContext(?Throwable $exception) {
if ($this->isCommitted || $this->isRolledBack) {
return;
}
if ( $exception === null ) {
$this->logger->debug('Implicit commit - consider calling
commit() for clearer code.');
$this->commit();
} else {
$this->logger->debug('Implicit rollback - consider calling
rollback() for clearer code.');
$this->rollback();
}
}
--
Rowan Tommins
[IMSoP]
The choice of adding the exception to the exitContext() is
interesting, but also very opinionated:
- It means, that the only way to abort, in non-exceptional cases, is
to throw yourself an exception. And put a try/catch around the with()
{} block. Or manually use enterContext() & exitContext() - with a fake
"new Exception" essentially.- Maybe you want to hold a transaction, but just ensure that
everything gets executed together (i.e. atomicity), but not care about
whether everything actually went through (i.e. not force a rollback on
exception). You'll now have to catch the exception, store it to a
variable, use break and check for the exception after the with block.
Or, yes, manually using enterContext() and exitContext().The Context Manager is given knowledge of the exception, but it's not
obliged to change its behaviour based on that knowledge. I don't think
that makes the interface opinionated, it makes it extremely flexible.It means you can write this, which is impossible in a destructor:
function exitContext(?Throwable $exception) {
if ( $exception === null ) {
$this->commit();
} else {
$this->rollback();
}
}But you could also write any of these, which are exactly the same as
they would be in __destruct():// Rollback unless explicitly committed
function exitContext(?Throwable $exception) {
if ( ! $this->isCommitted ) {
$this->rollback();
}
}// Expect explicit commit or rollback, but roll back as a safety net
function exitContext(?Throwable $exception) {
if ( ! $this->isCommitted && ! $this->isRolledBack ) {
$this->logger->warn('Transaction went out of scope without
explicit rollback, rolling back now.');
$this->rollback();
}
}// User can choose at any time which action will be taken on destruct / exit
function exitContext(?Throwable $exception) {
if ( $this->shouldCommitOnExit ) {
$this->commit();
} else {
$this->rollback();
}
}You could also combine different approaches, using the exception as an
extra signal only if the user hasn't chosen explicitly:function exitContext(?Throwable $exception) {
if ($this->isCommitted || $this->isRolledBack) {
return;
}
if ( $exception === null ) {
$this->logger->debug('Implicit commit - consider calling
commit() for clearer code.');
$this->commit();
} else {
$this->logger->debug('Implicit rollback - consider calling
rollback() for clearer code.');
$this->rollback();
}
}--
Rowan Tommins
[IMSoP]
Though one point to note here, $this->isCommitted on the context MANAGER is not the same as isCommitted on the context VARIABLE. So in the above examples you would have to either return $this from enterContext() (which is fine), or save a reference to the context variable in the manager and then check $this->txn->isCommitted (which is also fine).
Which you choose is mostly an implementation detail, and it's fine either way; I just want to emphasize that a lot of the flexibility of context managers comes from the separation of the context manager from the variable.
A context manager is not just an auto-unsetter, though that is part of what it does. It is more properly a way to abstract out and package up setup/teardown lifecycle management, which can differentiate between a success or failure case. (At least as much as PHP itself is able to right now.) That has a wide variety of use cases, only a few of which a simple destructor could handle.
--Larry Garfield
Hey Larry, Tim, Seifeddine and Arnauld,
Arnaud and I would like to present another RFC for consideration: Context Managers.
https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from Tim and Seifeddine. Both proposals grew out of casual discussion several months ago; I don't believe either team was aware that the other was also actively working on such a proposal, so we now have two. C'est la vie. :-)
Naturally, Arnaud and I feel that our approach is the better one. In particular, as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters. It also does not allow differentiating between a success or failure exit condition, which for many use cases is absolutely mandatory (as shown in the examples in the context manager RFC).
The Context Manager proposal is a near direct port of Python's approach, which is generally very well thought-out. However, there are a few open questions as listed in the RFC that we are seeking feedback on.
Discuss. :-)
I've been looking at both RFCs and I don't think either RFC is good
enough yet.As for this RFC:
It makes it very easy to not call the exitContext() method when calling
enterContext() manually. The language (obviously) doesn't prevent
calling enterContext() - and that's a good thing. But also, it will not
enforce that exitContext() gets ever called (and it also cannot,
realistically).Thus, we have a big pitfall, wherein APIs may expect enterContext() and
exitContext() to be called in conjunction, but users don't - with
possibly non-trivial side-effects (locks not cleared, transactions not
committed etc.). Thus, to be safe, implementers of the interface will
also likely need the destructor to forward calls to exitContext() as
well. But it's an easy thing to forget - after all, the intended usage
of the API just works. Why would I think of that, as an implementer of
the interface, before someone complains?Ultimately you definitely will need the capability of calling
enterContext() and exitContext() manually too (i.e. restricting that is
not realistic either), as lifetimes do not necessarily cleanly nest - as
a trivial example, you might want to obtain access to a handle which is
behind a lock. You'll have to enter the context of the lock, enter the
context of the handle, and close the lock (because more things are
locked behind that lock, including the handle). ... But you don't
necessarily want the hold on the lock to outlive the inner handle. In
short: The proposed approach only allows nesting contexts, but not
interleaving them.Further, calling with() twice on an object is quite bad in general. But
it might easily happen - you have a function which wants a transaction.
e.g. function writeSomeData(DatabaseTransaction $t) { with ($t) {
$t->query("..."); } }. A naive caller might think, DatabaseTransaction
implements ContextManager ... so let's wrap it: with($db->transaction()
as $t) { writeSomeData($t); }. But now you are nesting a transaction,
which may have unexpected side effects - and the code probably not
prepared to handle it. So, you have to add yet another safeguard into
your implementation: check whether enterContext() is only active once.
... Or, maybe a caller assumes that $t = $db->transaction(); with ($t) {
$t->query("..."); } with ($t) { $t->query("..."); } is fine - but the
implementation is not equipped to handle multiple calls to enterContext().Additionally, I would expect implementers to want to provide methods,
which can be called while the context is active. However, it's not
impossible to call these methods without wrapping it into with() or
calling the enterContext() method explicitly. One more failure mode,
which needs handling.
Like for example, calling $t->query() on a transaction without starting it.I don't like that design, which effectively forces you to put safety
checks for all but the simplest cases onto the ContextManager
implementation.
And it forces the user to recognize "this returned object
DatabaseTranscation actually implements ContextManager, thus I should
put it into with() and not immediately call methods on it". (A problem
which the use() proposal from Tim does not have by design.)The choice of adding the exception to the exitContext() is interesting,
but also very opinionated:
- It means, that the only way to abort, in non-exceptional cases, is to
throw yourself an exception. And put a try/catch around the with() {}
block. Or manually use enterContext() & exitContext() - with a fake "new
Exception" essentially.- Maybe you want to hold a transaction, but just ensure that everything
gets executed together (i.e. atomicity), but not care about whether
everything actually went through (i.e. not force a rollback on
exception). You'll now have to catch the exception, store it to a
variable, use break and check for the exception after the with block.
Or, yes, manually using enterContext() and exitContext().It feels like with() is designed to be covering 70% of the use cases,
with a load of hidden pitfalls and advanced usage requiring manual
enterContext() and exitContext() calls. It's not a very good solution.As to the destructors (and also in reply to that other email from
Arnauld talking about PDO::disconnect() etc.):It's already possible today to have live objects which are already
destructed. It's extremely common to have in shutdown code. It's
sometimes a pain, I agree. But it's an already known pain, and an
already handled pain in a lot of code.
If your object only knows "create" and "destruct", there's no way for a
double enterContext() (nested or consecutive) situation to ever happen.
(Well, yes, you could theoretically manually call __destruct(), but
why would you ever do that?)Last thing - proper API usage forces you to use that construct.
To the use() proposal from Tim:
This proposal makes it very simple to inadvertently leak the use()'d
value. I don't think the current proposed form goes far enough.However we could decide to force-destruct an object (just like we do in
shutdown too). It's just one single flag for child methods to check as
well - the object is either destructed or not. We could also trivially
prohibit nested use() calls by throwing an AlreadyDestructedError when
an use()'d and inside destructed object crosses the use() boundary.The only disadvantage is that there's no information about thrown
exceptions. I.e. you cannot add a default behaviour of "on exception,
please do this", like rolling transactions back. But:
- Is it actually a big problem? Where is the specific disadvantage over
simply $db->transaction(function($t) { /* do stuff */ }); - where the
call of the passed Closure can be trivially wrapped in try/catch.- If yes, can we e.g. add an interface ExceptionDestructed { public bool
$destructedDuringException; }? Which will set that property if the
property is still undefined - to true if the destructor gets triggered
inside ZEND_HANDLE_EXCEPTION. To false otherwise. And, if an user
desires to manually force success/failure handling, he may set
$object->destructedDuringException = true; himself as a very simple -
one-liner - escape hatch.The use() proposal is not a bad one, but I feel like requiring the RC to
drop to zero first, misses a bit of potential to save users from mistakes.
The other nice thing about use() is that it's optional. You don't have
to use it. You use it if you want some scoping, otherwise the scope is
simply the function scope.To both proposals:
It remains possible by default to call methods on the object, after
leaving the with or use block. So some checking on methods for a safe
API is probably still required.I don't think it's possible to solve that problem at all with the
ContextManager RFC, except manual checking by the implementer in every
single method. But it's possibly possible to solve it with the use() RFC
in orthogonal ways - like a #[ProhibitDestructed] attribute, which can
be added onto a class (making it apply to all methods) or a specific
method and causes method calls to throw an exception when the object is
destructed.
Which is possible to provide by the language, as the language knows
about whether objects are already destructed, unlike e.g. the
ContextManager, where it would be object state, which has to be
maintained by the user.TL;DR: ContextManagers are a buttload of pitfalls. use() is probably
better, with much less inherent problems. And with the remaining
problems of the proposal being actually solvable.Thanks,
Bob
Hi Bob,
I agree with your points, especially this one:
It makes it very easy to not call the exitContext() method when calling enterContext() manually. [...] Thus, we have a big pitfall...
This is the exact reason why, in the use() thread, I suggested a
Disposable interface should not have an enter() method. The
language should just guarantee a single dispose() method is called
when the scope is exited (successfully or not). This completely avoids
the "unbalanced call" pitfall. SA tools can warn/error when
dispose() is called manually. API authors should be aware that the
resource might receive multiple dispose() calls and handle this
gracefully.
Regarding your other valid concerns (nested calls, using objects after
the block, "leaking" the variable), I believe those are all solvable
by how the use() construct evolves, which is why I prefer its
foundation.
You noted:
It remains possible by default to call methods on the object, after leaving the with or use block. So some checking on methods for a safe API is probably still required.
You're right, but the use() proposal has a clear path to solve this,
which is far superior IMO to the manual checks required by the
ContextManager design. As discussed in the use() thread, we could
later introduce:
-
A
Resourcemarker interface: This would tell the engine to
handle the object specially (e.g., use weak references in backtraces
to prevent accidental refcount inflation). -
A
Disposableinterface: This would have thedispose()
logic and could (and probably should) also be aResource.
Furthermore, a Disposable interface opens up the possibility of
being supported outside the use() construct entirely. The engine
could guarantee dispose() is called whenever the object goes out of
any scope, just like a destructor but with crucial exception
awareness:
function x() {
$disposable = new SomeDisposable();
return; // $disposable->dispose(null) called
}
function y() {
$disposable = new SomeDisposable();
throw new Error(); // $disposable->dispose($error) called
}
This would make Disposable a truly powerful, general-purpose RAII
mechanism, not just a feature tied to use().
With these (future) additions, the use() construct could be enhanced
to enforce a no-escape policy specifically for Resources. If
use($r = get_resource()) finishes and $r still has references, the
engine could throw an error.
This combination would programmatically prevent the "use after free"
pitfall you described, rather than relying on manual checks inside
every single method.
All of those powerful safety checks can be added in the future. I
don't see why we need to cram them into the initial use() RFC. Its
current __destruct-based implementation is already useful today
for APIs designed to leverage RAII (like the Psl lock example). It's
normal that few APIs are designed this way now; the feature doesn't
exist yet. Psl just happens to support it because of its Hack origins.
This seems like a much safer and more extensible path.
This is the exact reason why, in the
use()thread, I suggested a
Disposableinterface should not have anenter()method. The
language should just guarantee a singledispose()method is called
when the scope is exited (successfully or not).
Remember, the Context Manager is not the same as the resource being
managed, and exitContext() is not equivalent to dispose().
In fact, it occurs to me that a C#-style using() block can be written as
a Context Manager with only a few lines of code:
interface Disposable {
public function dispose(): void;
}
class Disposer implements ContextManager
{
private function __construct(private Disposable $resource) {}
public static function of(Disposable $resource): ContextManager {
return new self($resource);
}
public function enterContext(): Disposable {
return $this->resource;
}
public function exitContext(): void {
$this->resource->dispose();
}
}
Then to use it, you just pass in whatever object you want:
with(Disposer::of(new MyDisposableResource) as $foo) {
// ...
$foo->whatever();
// ...
} // guaranteed call to $foo->dispose() here
Which is pretty close to a direct port of C#:
using($foo = new MyDisposableResource) {
// ...
$foo->whatever();
// ...
} // guaranteed call to $foo->dispose() here
You're right, but the
use()proposal has a clear path to solve this,
which is far superior IMO to the manual checks required by the
ContextManagerdesign. As discussed in theuse()thread, we could
later introduce:
A
Resourcemarker interface: This would tell the engine to
handle the object specially (e.g., use weak references in backtraces
to prevent accidental refcount inflation).A
Disposableinterface: This would have thedispose()
logic and could (and probably should) also be aResource.
Neither of these is specific to the use() block. #1 is just a general
language feature - as someone else pointed out, it could be similar to
#[SensitiveParameter]. #2 is, as shown above, trivial to implement using
a context manager, without even needing additional language support.
Furthermore, a
Disposableinterface opens up the possibility of
being supported outside theuse()construct entirely. The engine
could guaranteedispose()is called whenever the object goes out of
any scope, just like a destructor but with crucial exception
awareness:
This is an interesting idea, but again doesn't seem to have any special
relationship to the use() proposal. It's also not what C# or Hack mean
by "Disposable", so that would probably be a poor choice of name. It
sounds more like an extension of the current __destruct(), which could
potentially be as simple as adding a parameter to that.
With these (future) additions, the
use()construct could be enhanced
to enforce a no-escape policy specifically forResources. If
use($r = get_resource())finishes and$rstill has references, the
engine could throw an error.This combination would programmatically prevent the "use after free"
pitfall you described, rather than relying on manual checks inside
every single method.
Unless the error happens at compile time (probably impossible in PHP's
current workflow), or crashes the application with an uncatchable error
(yikes!), I don't think it's possible to make that guarantee. Consider
this code:
try {
use($r = get_resource()) {
SomeClass::$staticVar = $r;
}
}
catch ( Throwable $e ) {
log_and_continue($e);
}
SomeClass::$staticVar->doSomething();
What is the value of SomeClass::$staticVar? I can only see three options:
-
It is still the open resource from get_resource(); the call succeeds,
but the resource has leaked -
It is a closed resource, and the call to doSomething() throws an
"object already disposed" Error -
It has been forcibly unset, and the call to doSomething() throws a
"method call on null" Error
(I don't think #3 is actually possible in the current Engine, because we
only track the reference count, not a reference list; but it's at
least theoretically possible.)
And that's leaving aside the fact that a lot of resources can end up in
unusable states anyway, such as a network connection being closed from
the other end.
All of those powerful safety checks can be added in the future. I
don't see why we need to cram them into the initialuse()RFC.
Requiring a "double opt-in" (the use() block and an interface) makes the
feature less reliable - you can't see at a glance if the use() block is
performing the extra cleanup, or just creating a variable scope.
I think it's better to have a specific block that can only be used
with objects implementing the appropriate interface, like
using+IDisposable (C# and Hack) or try+AutoCloseable (Java
https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html).
If we want a general-purpose "block scoping" feature alongside that, we
can discuss exactly how that should look and operate, as I replied to
Tim here: https://externals.io/message/129059#129188
--
Rowan Tommins
[IMSoP]
Hello all.
It makes it very easy to not call the exitContext() method when calling
enterContext() manually
Consider the code below:
class FileContext {
private $handle;
public function __construct(private string $filename, private
string $mode) {}
public function __enter() {
$this->handle = fopen($this->filename, $this->mode);
return $this->handle;
}
public function __exit(?Throwable $e = null) {
if ($this->handle) {
fclose($this->handle);
}
}
}
$ctx = new FileContext('example.txt', 'w');
$f = $ctx->__enter();
try {
fwrite($f, "Hello world");
} finally {
$ctx->__exit();
}
Question: what is the probability of making a mistake in this code?
What is the likelihood that a programmer will forget to call enter and exit?
...
with (new FileContext('example.txt', 'w')) as $f {
fwrite($f, "Hello world");
}
In this case, the probability of forgetting to call enter or exit is
zero, since the language now supports this paradigm at the syntax
level.
Question: what is the probability of accidentally calling a method
instead of not calling it?
...
with (new FileContext('example.txt', 'w')) as $f {
$f->__enter(); // Error!
fwrite($f, "Hello world");
}
I believe that finding a clear mathematical proof in this case is impossible.
But if we look at studies on error statistics, it is the absence of a
call that is the most common problem.
(https://tomgu1991.github.io/assets/files/19compsac.pdf)
Thus, we have a big pitfall, wherein APIs may expect enterContext() and exitContext()
Is it really a big pitfall? If so, then functions likefopenand
fcloseshould be removed from the language altogether,
because their existence is an even bigger pitfall.
Ultimately you definitely will need the capability of calling enterContext() and exitContext() manually too (i.e. restricting that is not realistic either)
Of course, they can be restricted: make the methods private, so that
only PHP itself can call them, and they cannot be accidentally called
anywhere else outside the class.
But I don't see any convincing reasons for doing that.
The proposed approach only allows nesting contexts, but not interleaving them.
Why does this need to be done at all?
I don't know what "interleaving contexts" means in practice.
But even if they do exist, that goes beyond the scope of the current proposal.
Nevertheless, it is worth noting that studying Python bugs for this
RFC is a good thing:
https://bugs.python.org/issue29988 - with statements are not ensuring
that exit is called if enter succeeds.
Also:
https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager
Total:
Advantages of with:
- better semantics than try–catch
- less control-flow code
It would be strange to demand that with be safer than the rest of PHP code.
That’s an odd requirement. Of course, if PHP were a compiled language,
many checks could be done at compile time.
But we have static analysis for that.
Best regards, Ed
Arnaud and I would like to present another RFC for consideration: Context Managers.
I haven't had a chance to read the RFC in detail yet, but am really
pleased to see it. Ever since I read the description (and design
rationale) for Python's implementation, I have been thinking this would
be a useful addition to PHP.
One small thing I noticed: you list "Generator decorator managers" in
Future Scope, and while I agree that a magic attribute would need a bit
of thought, including the "standard boilerplate class" under some
suitable name seems worth considering.
--
Rowan Tommins
[IMSoP]
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
--
Larry Garfield
larry@garfieldtech.com
I didn't mention this explicitly, so I'll do that now:
- We have changed the keyword from
withtousing, to avoid conflicting with existing global functions in Laravel. - As requested, we've added support for multiple context managers in a single block. They simply transpile to nested try-catch-finally blocks.
There's still 2 outstanding questions:
- Is there any interest in turning
usinginto an expression rather than a statement, so that it can be used in expression contexts? - Does anyone want to argue about how
continueshould behave? Right now it matchesswitch, for better or worse. We're happy going with whatever the consensus is. If there's no feedback or consensus, we'll go with the current implementation.
To me, Rowan's experimentation really shows the value of splitting the manager variable from the context variable. The amount of flexibility gained is dramatic, and it makes many implementations much easier.
--Larry Garfield
- Is there any interest in turning
usinginto an expression rather than a statement, so that it can be used in expression contexts?
What would it evaluate to? The value returned by exitContext()? That's
already controlling whether caught exceptions are rethrown, so the only
value that would ever reach an enclosing expression would be "true".
(I've sometimes thought about making return an expression in the same
spirit as throw. Its evaluated type would of course be "never" since
any enclosing expression would be abandoned.)
Morgan
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
More updates to Context Managers:
- We have added "masking" for the context variable, using essentially the same technique as the block scope RFC.
- We have added support for
try using, as a shorthand for when you want to wrap a try-catch-finally around a using statement anyway.
More details of both are in the RFC.
As no one seems to have a strong opinion on continue, we will most likely proceed with the current approach of matching switch behavior.
There doesn't seem to be much interest in making using an expression, which I find unfortunate, but that means we'll probably drop that. Fortunately it is probably possible to change in the future if the need arises (the way throw was changed).
--Larry Garfield
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
More updates to Context Managers:
- We have added "masking" for the context variable, using essentially
the same technique as the block scope RFC.- We have added support for
try using, as a shorthand for when you
want to wrap a try-catch-finally around a using statement anyway.More details of both are in the RFC.
As no one seems to have a strong opinion on
continue, we will most
likely proceed with the current approach of matchingswitchbehavior.There doesn't seem to be much interest in making
usingan expression,
which I find unfortunate, but that means we'll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the waythrowwas changed).--Larry Garfield
Since the only feedback on what to use for "as" was that => makes sense, we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}
--Larry Garfield
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
More updates to Context Managers:
- We have added "masking" for the context variable, using essentially
the same technique as the block scope RFC.- We have added support for
try using, as a shorthand for when you
want to wrap a try-catch-finally around a using statement anyway.More details of both are in the RFC.
As no one seems to have a strong opinion on
continue, we will most
likely proceed with the current approach of matchingswitchbehavior.There doesn't seem to be much interest in making
usingan expression,
which I find unfortunate, but that means we'll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the waythrowwas changed).--Larry Garfield
Since the only feedback on what to use for "as" was that => makes sense,
we have changed the RFC to use => instead. So the new syntax isusing (new CM() => $cVar) {
// Do stuff here.
}
Going to be controversial here, but this is confusing, because it operates
in the exact opposite of every other usage of => we have. With associative
arrays, the left is assigned to the expression on the right; with arrow
functions, the return value is the expression on the right; with match, the
expression on the right is returned.
This is going to be easy to get wrong.
--
Matthew Weier O'Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him
Since the only feedback on what to use for "as" was that => makes sense,
we have changed the RFC to use => instead. So the new syntax isusing (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it operates
in the exact opposite of every other usage of => we have. With associative
arrays, the left is assigned to the expression on the right; with arrow
functions, the return value is the expression on the right; with match, the
expression on the right is returned.This is going to be easy to get wrong.
I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }I think it’s still clear what this is doing, when reading it.
Cheers,
Ben
I also agree with Matthew but the reversed proposed here looks very very
awkward to me. I think the most natural thing is the “as” but I may have
missed the discussion on why it had to be changed.
Thinking of foreach ($array as $value), an item from the array (left) is
assigned to $value (right). That seems symmetrical to using (new Manager as
$manager) where the instance (left) is assigned to the variable (right).
Also when using aliases on the top of the file “use Foo as Bar;” as is also
assigning the left to the right.
Since the only feedback on what to use for "as" was that => makes sense, we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it operates in the exact opposite of every other usage of => we have. With associative arrays, the left is assigned to the expression on the right; with arrow functions, the return value is the expression on the right; with match, the expression on the right is returned.
This is going to be easy to get wrong.
I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }I think it’s still clear what this is doing, when reading it.
Cheers,
BenI also agree with Matthew but the reversed proposed here looks very very awkward to me. I think the most natural thing is the “as” but I may have missed the discussion on why it had to be changed.
Thinking of foreach ($array as $value), an item from the array (left) is assigned to $value (right). That seems symmetrical to using (new Manager as $manager) where the instance (left) is assigned to the variable (right). Also when using aliases on the top of the file “use Foo as Bar;” as is also assigning the left to the right.
This argument makes sense to me (i.e., using as instead of =>).
I’ll go back through the thread to find the arguments against using as, to understand why it changed to =>.
Cheers,
Ben
(Replying to a few of you in one go, I hope the formatting is clear)
we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it operates
in the exact opposite of every other usage of => we have.I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }
It took me a long time to figure out what you were both saying here, because to me the direction of the arrow seems to consistently indicate data flow: you call the function, and data comes out; you enter the context, and a variable comes out.
But I think I see it now: you're treating the context variable like an array key, that is somehow "bound to" the result. Except that's not what's happening, otherwise we could just use "=".
I also agree with Matthew but the reversed proposed here looks very very
awkward to me. I think the most natural thing is the “as” but I may have
missed the discussion on why it had to be changed.Thinking of foreach ($array as $value), an item from the array (left) is
assigned to $value (right).
This is the way to read the "as" syntax, yes; as I've said in a few places, a Context Manager is like an iterator that goes around once.
That seems symmetrical to using (new Manager as
$manager) where the instance (left) is assigned to the variable (right).
This, however, is why it was changed: that is not what is happening. The Context Manager is not assigned to the variable, it produces a value which is assigned to the variable.
Again, look at the foreach equivalence: you wouldn't write "foreach(new MyIterator as $iterator)", because it's not the iterator that ends up in the variable, it's the items produced by the iterator.
So you have "foreach(new MyIterator as $item)" and "using(new MyContextManager as $contextVar)".
But evidently that is not obvious to readers.
My favourite among my own suggestions was "using($cValue from new CM())", but Larry didn't like the reversed order. I also suggested 'using(new CM() for $cValue)", but I don't think that's as clear.
Perhaps there is some other word we can use? I suspect it would not need to be fully reserved, as it's used in a very restricted context.
Or, perhaps there's something other than "using" that clues the reader into "this object will be used to produce an arbitrary value" in the same way as "foreach...as" does?
Rowan Tommins
[IMSoP]
Hi
Am 2025-12-16 09:08, schrieb Rowan Tommins [IMSoP]:
I also agree with Matthew but the reversed proposed here looks very
very
awkward to me. I think the most natural thing is the “as” but I may
have
missed the discussion on why it had to be changed.Thinking of foreach ($array as $value), an item from the array (left)
is
assigned to $value (right).This is the way to read the "as" syntax, yes; as I've said in a few
places, a Context Manager is like an iterator that goes around once.
That definition doesn't make sense to me. An iterator that only ever
emits a single value is not actually iterating anything and it would
never occur me to interpret it like that. As I had also noted in my
reply https://news-web.php.net/php.internals/129582, it is absolutely
magic to me that break; would target using().
My favourite among my own suggestions was "using($cValue from new
CM())", but Larry didn't like the reversed order. I also suggested
'using(new CM() for $cValue)", but I don't think that's as clear.
FWIW: Using from would be somewhat consistent with yield from. Using
for would effectively invent another meaning for an existing keyword,
something that folks disliked for use() as the initial block scoping
keyword.
Best regards
Tim Düsterhus
That definition doesn't make sense to me. An iterator that only ever
emits a single value is not actually iterating anything and it would
never occur me to interpret it like that.
Well, firstly, it is certainly possible to have an iterator that only
emits one item; or even no items at all. There are even design patterns
that rely on that - in some languages, the Option/Maybe type is a
zero-or-one-item iterator, and you unwrap the value using "map" to
either call or not call a function.
But that wasn't really the image I was trying to conjure; it's more
about the relationship between the Iterator and the item. Here's a
metaphor which might or might not work:
You are at an arcade. There is a machine with a single button, and a
tray. Every time you press the button, a toy drops into the tray. After
you press the button a few times, toys stopping coming out, because
there are none left in the machine.
This machine is labelled "Iterator".
Next to it, there is another machine which looks similar. When you press
the button on this one, some music starts, and then a toy drops into the
tray. When you press the button again, the music stops. No more toys drop.
The toys are not making the music, and the music is not making the toys;
they just both happen when you press the button.
This machine is labelled "Context Manager".
Then there's a third machine, which doesn't have the tray, only the
button. You press the button once, and the music starts; you press it
again, and it stops.
This machine is also labelled "Context Manager".
That's what I mean by "like an iterator with one item": that Iterators
and Context Managers are similar kind of machines. The "foreach" and
"using" statements don't create or move those machines, they operate
them, pressing the button so the music plays and the next toy comes out.
--
Rowan Tommins
[IMSoP]
My favourite among my own suggestions was "using($cValue from new CM())", but Larry didn't like the reversed order. I also suggested 'using(new CM() for $cValue)", but I don't think that's as clear.
Perhaps there is some other word we can use? I suspect it would not need to be fully reserved, as it's used in a very restricted context.
I guess a symmetric counterpart to using "from" would give
"using(new CM() to $cValue)". Maybe "into".
On Tue, Dec 16, 2025 at 5:10 AM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
(Replying to a few of you in one go, I hope the formatting is clear)
we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it
operates
in the exact opposite of every other usage of => we have.I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }It took me a long time to figure out what you were both saying here,
because to me the direction of the arrow seems to consistently indicate
data flow: you call the function, and data comes out; you enter the
context, and a variable comes out.But I think I see it now: you're treating the context variable like an
array key, that is somehow "bound to" the result. Except that's not what's
happening, otherwise we could just use "=".I also agree with Matthew but the reversed proposed here looks very very
awkward to me. I think the most natural thing is the “as” but I may have
missed the discussion on why it had to be changed.Thinking of foreach ($array as $value), an item from the array (left) is
assigned to $value (right).This is the way to read the "as" syntax, yes; as I've said in a few
places, a Context Manager is like an iterator that goes around once.That seems symmetrical to using (new Manager as
$manager) where the instance (left) is assigned to the variable (right).This, however, is why it was changed: that is not what is happening. The
Context Manager is not assigned to the variable, it produces a value
which is assigned to the variable.Again, look at the foreach equivalence: you wouldn't write "foreach(new
MyIterator as $iterator)", because it's not the iterator that ends up in
the variable, it's the items produced by the iterator.So you have "foreach(new MyIterator as $item)" and "using(new
MyContextManager as $contextVar)".But evidently that is not obvious to readers.
My conclusion here is exactly the opposite of yours because this
explanation makes as even more symmetrical now. As you've mentioned
foreach(new MyIterator as $iterator) is not how it works. An iterator
produces a value that is assigned to the variable after as.
Symmetrically, using(new ContextManager as $context) the ContextManager
is also producing a value that is assigned to the variable after as. The
only difference is in the keyword: foreach will loop, but using will not
loop since ContextManager is not an iterator.
I'm assuming this would also be a valid syntax: using ($manager = new ContextManager as $context) which gives us a shot at capturing the context
manager.
I wrote a little snippet to try and see (using GitHub Gist Syntax
Highlighter) how some of the options would look like once IDE support /
syntax highlighting is baked in:
[image: image.png]
Ultimately, people will take a quick reading in a Context Manager
documentation and the parallels to explain how it works seems to be very
well in place for as, imo.
In the image above I tried adding for using because I was trying really
hard to come up with a word that could replace each in foreach to make
it even more symmetrical, but I could not find anything decent.
-
for context(new ContextManager as $context)
This one reads well, but the fact its two words (and a long word for that
matter) doesn't sit well. -
for with(new ContextManager as $context)
I'm not a native English speaker, but I think this doesn't read at all -
for one(new ContextManager as $context)
Kind of implies it works with iterators (similar to each), which is not
exactly the case and the language has very little motivation to create an
iterator that loops only once just to make this work. -
for using(new ContextManager as $context)
Similar thoughts asfor context, the only difference that makes me like
this one more is becausefor contextdoesn't sit well withtry context
whilefor usingcould go well withtry using.
In any case, the longer I sit with this the more I dislike => for this.
In an array, => is an assignment of key/value pairs.
For foreach ($iterator as $key => $value), the => is a destructing
instruction that aligns with how the array was originally assigned.
For the arrow function, it's kind of implied in the name ARROW function
that we would need a syntax in the lines of fn () =>. I don't think this
relates well with arrays, but it works well to me because of how universal
arrow functions are in Javascript.
For match ($value), it reads as WHEN ($value) MATCH '' THEN. The arrow here =>` is the THEN.
My conclusion is that => is either a value placement (for arrays) or an
instruction execution (for arrow function and match). As such, If => were
to be incorporated into Context Managers, I think it would make much more
sense like this:
using (new ContextManager as $context) => [single line expression] where
the arrow creates a symmetry with how arrow functions work.
--
Marco Deleu
On Tue, Dec 16, 2025 at 5:10 AM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:(Replying to a few of you in one go, I hope the formatting is clear)
we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it operates
in the exact opposite of every other usage of => we have.I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }It took me a long time to figure out what you were both saying here, because to me the direction of the arrow seems to consistently indicate data flow: you call the function, and data comes out; you enter the context, and a variable comes out.
But I think I see it now: you're treating the context variable like an array key, that is somehow "bound to" the result. Except that's not what's happening, otherwise we could just use "=".
I also agree with Matthew but the reversed proposed here looks very very
awkward to me. I think the most natural thing is the “as” but I may have
missed the discussion on why it had to be changed.Thinking of foreach ($array as $value), an item from the array (left) is
assigned to $value (right).This is the way to read the "as" syntax, yes; as I've said in a few places, a Context Manager is like an iterator that goes around once.
That seems symmetrical to using (new Manager as
$manager) where the instance (left) is assigned to the variable (right).This, however, is why it was changed: that is not what is happening. The Context Manager is not assigned to the variable, it produces a value which is assigned to the variable.
Again, look at the foreach equivalence: you wouldn't write "foreach(new MyIterator as $iterator)", because it's not the iterator that ends up in the variable, it's the items produced by the iterator.
So you have "foreach(new MyIterator as $item)" and "using(new MyContextManager as $contextVar)".
But evidently that is not obvious to readers.
My conclusion here is exactly the opposite of yours because this
explanation makesaseven more symmetrical now. As you've mentioned
foreach(new MyIterator as $iterator)is not how it works. An iterator
produces a value that is assigned to the variable afteras.
Symmetrically,using(new ContextManager as $context)the
ContextManager is also producing a value that is assigned to the
variable afteras. The only difference is in the keyword: foreach
will loop, but using will not loop since ContextManager is not an
iterator.I'm assuming this would also be a valid syntax:
using ($manager = new ContextManager as $context)which gives us a shot at capturing the
context manager.I wrote a little snippet to try and see (using GitHub Gist Syntax
Highlighter) how some of the options would look like once IDE support /
syntax highlighting is baked in:image.png
Ultimately, people will take a quick reading in a Context Manager
documentation and the parallels to explain how it works seems to be
very well in place foras, imo.In the image above I tried adding
for usingbecause I was trying
really hard to come up with a word that could replaceeachin
foreachto make it even more symmetrical, but I could not find
anything decent.
for context(new ContextManager as $context)
This one reads well, but the fact its two words (and a long word for
that matter) doesn't sit well.
for with(new ContextManager as $context)
I'm not a native English speaker, but I think this doesn't read at all
for one(new ContextManager as $context)
Kind of implies it works with iterators (similar to each), which is not
exactly the case and the language has very little motivation to create
an iterator that loops only once just to make this work.
for using(new ContextManager as $context)
Similar thoughts asfor context, the only difference that makes me
like this one more is becausefor contextdoesn't sit well withtry contextwhilefor usingcould go well withtry using.In any case, the longer I sit with this the more I dislike
=>for this.In an array,
=>is an assignment of key/value pairs.
For foreach ($iterator as $key => $value), the => is a destructing
instruction that aligns with how the array was originally assigned.
For the arrow function, it's kind of implied in the name ARROW function
that we would need a syntax in the lines offn () =>. I don't think
this relates well with arrays, but it works well to me because of how
universal arrow functions are in Javascript.
For match ($value), it reads asWHEN ($value) MATCH '' THEN. The arrow here=>` is the THEN.My conclusion is that
=>is either a value placement (for arrays) or
an instruction execution (for arrow function and match). As such, If =>
were to be incorporated into Context Managers, I think it would make
much more sense like this:
using (new ContextManager as $context) => [single line expression]
where the arrow creates a symmetry with how arrow functions work.--
Marco Deleu
First, yeesh, where were y'all 2 weeks ago? :-P
Second, I find it fascinating that there's so many different mutually-incompatible spins on what => means. As argued in the short-functions and auto-capture-closure RFCs from a few years ago, => in nearly all cases currently means "evaluates to."
$arr = [
'a' => 'A",
'b' => 'B',
];
$arr['a'] // This "evaluates to" A
match ($foo) {
'a' => 'A',
'b' => 'B', // The 'b' case "evaluates to" B
};
fn($foo) => 'bar'; // This function "evaluates to" bar
foreach ($arr as $k => $v) {}
The last is a little bit inconsistent, but can still be read as "$k, and the thing that $k evaluates to."
So given that, and as Rowan noted the context manager is NOT the context variable:
using ($cm evaluates to $cv) {}
That's the meaning we're going for.
If there's some better word than =>, we're still open to it, but I am going to insist on left-to-right reading. $cm is created first, then it produces $cv.
'as' is what we had originally, since that's what Python used. However, it was pointed out that it was confusing as it implied the CM and CV were the same thing, or rather the expression on the left gets assigned to $cv, which is not the case. Hence the change.
A symbol does have the (dis?)advantage of a somewhat squishier meaning than a keyword.
--Larry Garfield
First, yeesh, where were y'all 2 weeks ago? :-P
I searched for the as vs => discussion on the current thread (ref:
https://externals.io/message/129077) and it didn't exist until now. Then I
found the discussion happened on the Examples comparing Block Scoped RAII
and Context Managers (ref: https://externals.io/message/129251). I wasn't
following that discussion because I thought it was mostly about comparing
both implementations and taking knowledge from it. I didn't expect that
to make this RFC to change without it being discussed here.
Second, I find it fascinating that there's so many different
mutually-incompatible spins on what => means. As argued in the
short-functions and auto-capture-closure RFCs from a few years ago, => in
nearly all cases currently means "evaluates to."$arr = [
'a' => 'A",
'b' => 'B',
];$arr['a'] // This "evaluates to" A
match ($foo) {
'a' => 'A',
'b' => 'B', // The 'b' case "evaluates to" B
};fn($foo) => 'bar'; // This function "evaluates to" bar
foreach ($arr as $k => $v) {}
The last is a little bit inconsistent, but can still be read as "$k, and
the thing that $k evaluates to."
I think the problem might be less about the true meaning of => and more
about the context that it applies to. You can see that in 3 cases of the
arrow it involves some sort of key/value pair (2 array scenarios and 1
match/case). In the case of arrow function, like I said before, arrow
functions are kind of a standard that stand up on their own. And even then,
the "evaluates to" is only partially because in arrow functions the
evaluation does not take immediate effect, it is parsed into a Closure that
needs to be invoked. For instance:
$array = [
'key' => throw new Exception('Stop'),
];
Here, the arrow evaluates to an exception that is immediately thrown.
match ($foo) {
'a' => throw new Exception('Stop because of A'),
'b' => throw new Exception('Stop because of B'),
};
Here, the evaluation is more loose and it only takes effect when there is a
match, but it does take effect.
fn () => throw new Exception('Stop');
Here, the evaluation never takes effect if the Closure is not invoked.
In these 3 cases, we can see each having its own evaluation rule:
immediate, immediate only for the match, and delayed. If I were to put into
words, I'd say:
array keys: evaluates to
match: evaluate to the matched key
arrow function: parse the syntax, does not evaluate to.
foreach ($arr as $k => $v) {}
Here I don't see it only as a bit inconsistent because it's not really
evaluating anything anymore. It is assigning a value. And this is where it
blows up for me. The first 3 cases we can argue semantics and point of
views, but they are minor nuances. Still, they're all cases where something
on the right is being evaluated for a reason on the left. On foreach, $v
stands on its own. It's not affected / impacted / dicated / evaluated
towards the left at all. It's not a bit inconsistent, it's completely
different. And the syntax for using () is largely similar to foreach, which
is why it should use as and not =>.
foreach ($iterator as $key => $value)
using ($contextManager as $context)
We can see that the only place where arrow ("evaluates to" / =>) is used
inside an instruction is in foreach and the syntax is split away from the
array itself because the keyword as is separating $k => $v from the
$iterator. I can totally see an argument for:
using (new ContextManager as $contextManager which evaluates to $context)
using (new ContextManager as $contextManager => $context)
Here, the return of enter would be [$this, $context] and the syntax falls
into place with a symmetry from foreach. I wouldn't strongly push for this
syntax because the following also works
using ($manager = new ContextManager as $context)
In any case, I don't have a vote and I really like the RFC in general. With
proper IDE support, I'm sure we will all get used to =>, but from where I
stand that's going to be really awkward for the PHP syntax.
--
Marco Deleu
I think the problem might be less about the true meaning of => and more
about the context that it applies to. You can see that in 3 cases of
the arrow it involves some sort of key/value pair (2 array scenarios
and 1 match/case). In the case of arrow function, like I said before,
arrow functions are kind of a standard that stand up on their own. And
even then, the "evaluates to" is only partially because in arrow
functions the evaluation does not take immediate effect, it is parsed
into a Closure that needs to be invoked. For instance:$array = [ 'key' => throw new Exception('Stop'), ];Here, the arrow evaluates to an exception that is immediately thrown.
match ($foo) { 'a' => throw new Exception('Stop because of A'), 'b' => throw new Exception('Stop because of B'), };Here, the evaluation is more loose and it only takes effect when there
is a match, but it does take effect.fn () => throw new Exception('Stop');Here, the evaluation never takes effect if the Closure is not invoked.
In these 3 cases, we can see each having its own evaluation rule:
immediate, immediate only for the match, and delayed. If I were to put
into words, I'd say:array keys: evaluates to
match: evaluate to the matched key
arrow function: parse the syntax, does not evaluate to.foreach ($arr as $k => $v) {}Here I don't see it only as a bit inconsistent because it's not really
evaluating anything anymore. It is assigning a value. And this is where
it blows up for me. The first 3 cases we can argue semantics and point
of views, but they are minor nuances. Still, they're all cases where
something on the right is being evaluated for a reason on the left. On
foreach, $v stands on its own. It's not affected / impacted / dicated /
evaluated towards the left at all. It's not a bit inconsistent, it's
completely different. And the syntax for using () is largely similar to
foreach, which is why it should useasand not=>.foreach ($iterator as $key => $value)
using ($contextManager as $context)We can see that the only place where arrow ("evaluates to" / =>) is
used inside an instruction is in foreach and the syntax is split away
from the array itself because the keywordasis separating$k => $v
from the $iterator. I can totally see an argument for:using (new ContextManager as $contextManager which evaluates to $context)
using (new ContextManager as $contextManager => $context)Here, the return of
enterwould be [$this, $context] and the syntax
falls into place with a symmetry from foreach. I wouldn't strongly push
for this syntax because the following also works
Well, in the vast majority case there will be no need to access the CM within the using block. If there is, then it's likely that enterContext() will be returning just $this already. So the $contextManager variable is unnecessary noise 99% of the time.
But if we take that out, which get right back to:
using (new ContextManager => $context)
I suppose grammatically this would make sense:
using (new CM() yields $v)
But that's a new keyword that is very close to an existing keyword that means something totally different, so probably not the best idea.
In any case, I don't have a vote and I really like the RFC in general.
Yay!
--Larry Garfield
On Tue, Dec 16, 2025 at 10:01 AM Larry Garfield larry@garfieldtech.com
wrote:
On Tue, Dec 16, 2025 at 5:10 AM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:(Replying to a few of you in one go, I hope the formatting is clear)
we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}Going to be controversial here, but this is confusing, because it
operates
in the exact opposite of every other usage of => we have.I agree with Matthew.
I think it makes more sense to reverse them, like this:
using ($cVar => new CM()) { // Do stuff here. }It took me a long time to figure out what you were both saying here,
because to me the direction of the arrow seems to consistently indicate
data flow: you call the function, and data comes out; you enter the
context, and a variable comes out.But I think I see it now: you're treating the context variable like an
array key, that is somehow "bound to" the result. Except that's not what's
happening, otherwise we could just use "=".I also agree with Matthew but the reversed proposed here looks very
very
awkward to me. I think the most natural thing is the “as” but I may
have
missed the discussion on why it had to be changed.Thinking of foreach ($array as $value), an item from the array (left)
is
assigned to $value (right).This is the way to read the "as" syntax, yes; as I've said in a few
places, a Context Manager is like an iterator that goes around once.That seems symmetrical to using (new Manager as
$manager) where the instance (left) is assigned to the variable
(right).This, however, is why it was changed: that is not what is happening.
The Context Manager is not assigned to the variable, it produces a value
which is assigned to the variable.Again, look at the foreach equivalence: you wouldn't write "foreach(new
MyIterator as $iterator)", because it's not the iterator that ends up in
the variable, it's the items produced by the iterator.So you have "foreach(new MyIterator as $item)" and "using(new
MyContextManager as $contextVar)".But evidently that is not obvious to readers.
My conclusion here is exactly the opposite of yours because this
explanation makesaseven more symmetrical now. As you've mentioned
foreach(new MyIterator as $iterator)is not how it works. An iterator
produces a value that is assigned to the variable afteras.
Symmetrically,using(new ContextManager as $context)the
ContextManager is also producing a value that is assigned to the
variable afteras. The only difference is in the keyword: foreach
will loop, but using will not loop since ContextManager is not an
iterator.I'm assuming this would also be a valid syntax:
using ($manager = new ContextManager as $context)which gives us a shot at capturing the
context manager.I wrote a little snippet to try and see (using GitHub Gist Syntax
Highlighter) how some of the options would look like once IDE support /
syntax highlighting is baked in:image.png
Ultimately, people will take a quick reading in a Context Manager
documentation and the parallels to explain how it works seems to be
very well in place foras, imo.In the image above I tried adding
for usingbecause I was trying
really hard to come up with a word that could replaceeachin
foreachto make it even more symmetrical, but I could not find
anything decent.
for context(new ContextManager as $context)
This one reads well, but the fact its two words (and a long word for
that matter) doesn't sit well.
for with(new ContextManager as $context)
I'm not a native English speaker, but I think this doesn't read at all
for one(new ContextManager as $context)
Kind of implies it works with iterators (similar to each), which is not
exactly the case and the language has very little motivation to create
an iterator that loops only once just to make this work.
for using(new ContextManager as $context)
Similar thoughts asfor context, the only difference that makes me
like this one more is becausefor contextdoesn't sit well withtry contextwhilefor usingcould go well withtry using.In any case, the longer I sit with this the more I dislike
=>for
this.In an array,
=>is an assignment of key/value pairs.
For foreach ($iterator as $key => $value), the => is a destructing
instruction that aligns with how the array was originally assigned.
For the arrow function, it's kind of implied in the name ARROW function
that we would need a syntax in the lines offn () =>. I don't think
this relates well with arrays, but it works well to me because of how
universal arrow functions are in Javascript.
For match ($value), it reads asWHEN ($value) MATCH '' THEN. The arrow here=>` is the THEN.My conclusion is that
=>is either a value placement (for arrays) or
an instruction execution (for arrow function and match). As such, If =>
were to be incorporated into Context Managers, I think it would make
much more sense like this:
using (new ContextManager as $context) => [single line expression]
where the arrow creates a symmetry with how arrow functions work.First, yeesh, where were y'all 2 weeks ago? :-P
Second, I find it fascinating that there's so many different
mutually-incompatible spins on what => means. As argued in the
short-functions and auto-capture-closure RFCs from a few years ago, => in
nearly all cases currently means "evaluates to."$arr = [
'a' => 'A",
'b' => 'B',
];$arr['a'] // This "evaluates to" A
match ($foo) {
'a' => 'A',
'b' => 'B', // The 'b' case "evaluates to" B
};fn($foo) => 'bar'; // This function "evaluates to" bar
foreach ($arr as $k => $v) {}
The last is a little bit inconsistent, but can still be read as "$k, and
the thing that $k evaluates to."
This is not at all how I read it. "evaluates to" is an assignment, and
assignments in PHP flow right to left.
In the case of an array, if I access the "a" key above, I get the value
"A". For your match example, if the match applies to "a", I get the value
"A". For an arrow function, I get the callable with the return. In other
words, the expression on the right is either evaluated or assigned to the
thing on the left.
With the proposed syntax, "using ($expression => $variable) { ... }", this
reads backwards: the expression is now on the left, and is captured to the
thing on the right. That's what I'm saying will be confusing.
So given that, and as Rowan noted the context manager is NOT the context
variable:using ($cm evaluates to $cv) {}
That's the meaning we're going for.
And again, that doesn't make sense from typical assignment rules. This is
why the other proposal's syntax ("let ($var = expression)") is not having
the same debate - the syntax is consistent with existing usage. It's also
consistent with how other scripting languages handle block scoping of
variables (see JS, shell scripting).
If there's some better word than =>, we're still open to it, but I am
going to insist on left-to-right reading. $cm is created first, then it
produces $cv.'as' is what we had originally, since that's what Python used. However,
it was pointed out that it was confusing as it implied the CM and CV were
the same thing, or rather the expression on the left gets assigned to $cv,
which is not the case. Hence the change.
I didn't find it ambiguous, but I can see where others might. That said, I
found it less ambiguous than => here.
A symbol does have the (dis?)advantage of a somewhat squishier meaning
than a keyword.I want block scoping; I have been bitten by accidental
re-assignment within a block many times, and hate having to come up with an
ever-so-slightly-different variable name to disambiguate. I don't care if
it's this proposal or the "let" proposal in terms of how the engine handles
it - but the syntax of "using (expression => $var)" isn't going to get my
vote due to how easily it can be written incorrectly.
I have no problem with the "using (expression as $var)" syntax, and the
"let($var = expression) syntax is just fine for me as well.
--
Matthew Weier O'Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him
If there's some better word than =>, we're still open to it, but I am going to insist on left-to-right reading. $cm is created first, then it produces $cv.
'as' is what we had originally, since that's what Python used. However, it was pointed out that it was confusing as it implied the CM and CV were the same thing, or rather the expression on the left gets assigned to $cv, which is not the case. Hence the change.
I didn't find it ambiguous, but I can see where others might. That
said, I found it less ambiguous than => here.A symbol does have the (dis?)advantage of a somewhat squishier meaning than a keyword.
I want block scoping; I have been bitten by accidental re-assignment
within a block many times, and hate having to come up with an
ever-so-slightly-different variable name to disambiguate. I don't care
if it's this proposal or the "let" proposal in terms of how the engine
handles it - but the syntax of "using (expression => $var)" isn't going
to get my vote due to how easily it can be written incorrectly.I have no problem with the "using (expression as $var)" syntax, and the
"let($var = expression) syntax is just fine for me as well.
I think it's important to realize that the two RFCs may be similar, but they're actually doing two VERY different things.
The let RFC is defining a guaranteed "unset this variable later". That's it.
The CM RFC is defining a way to package up setup and teardown logic into a reusable component. That happens to also involve unsetting a few variables, but that's not its primary design. The primary design mandates the separation of the context manager from context variable.
The CM RFC does substantially more than the let approach would be capable of, and is a model proven in Python to be useful and effective. (As I said elsewhere, I think let as an addition to existing structures would make more sense than using it for arbitrary new blocks, which would also avoid any confusion with CMs.)
So are you against the Context Manager proposal entirely, or would you be on board if we can find a non-confusing syntax? Ie,
using (new CM() which produces $var) {
}
If we can find a less verbose way to spell "which produces", would you be on board?
--Larry Garfield
I want block scoping; I have been bitten by accidental
re-assignment within a block many times, and hate having to come up
with an ever-so-slightly-different variable name to disambiguate. I
don't care if it's this proposal or the "let" proposal in terms of how
the engine handles it - but the syntax of "using (expression => $var)"
isn't going to get my vote due to how easily it can be written
incorrectly.
I want block scoping too. I don't think either of the current
proposals achieves that elegantly.
Context Managers aren't even trying to achieve it. Block scoping of the
context variable is a minor design detail which was added after the RFC
was shared with the list. You could in theory write a Context Manager
which took a value in its constructor and spat it out unchanged from its
"enter" method, but it would be a weird and wasteful way to create a
local variable.
The let() block proposal is about block scoping, but only for its own
special kind of block. I am not convinced by the reasons given for using
this unusual approach.
I would much rather we copy the precedent established by ALGOL 60 years
ago, and have variable declarations as statements which can appear in
the body of any block. In particular, I would probably just copy
JavaScript: allow "let $foo;" and "let $foo=expression;" anywhere, and
disallow use in the "temporal dead zone" between the start of the block
and the declaration.
However, I would also like Context Managers; which are a separate
feature, giving nice sugar for re-usable try-finally blocks.
--
Rowan Tommins
[IMSoP]
My conclusion here is exactly the opposite of yours because this
explanation makesaseven more symmetrical now.
I haven't exactly reached the "opposite" conclusion. If anything, I've
now reached the conclusion that "as" and "=>" are both equally ambiguous
and easy to misinterpret.
As you've mentioned
foreach(new MyIterator as $iterator)is not how
it works. An iterator produces a value that is assigned to the
variable afteras. Symmetrically,using(new ContextManager as $context)the ContextManager is also producing a value that is
assigned to the variable afteras. The only difference is in the
keyword: foreach will loop, but using will not loop since
ContextManager is not an iterator.
That is certainly how I read it, having already been primed by mentions
of iterators in the Python design document.
However, Tim pointed out (as you say, it came up in a different thread)
that the "each" is doing important work in the phrasing "for each ...
as". (It occurs to me that PHP used to have an actual function called
each(), which gave you an item from an array.)
Without that hint, the "X as Y" could easily be misread as meaning that
X and Y are the same thing.
In plain English, "using the screwdriver as a hammer" means the
screwdriver and the hammer are the same object. In PHP, "use Foo as
Bar;" means Foo and Bar refer to the same class.
Second, I find it fascinating that there's so many different mutually-incompatible spins on what => means.
I think we're all engaging in a lot of post hoc rationalisation for a
bunch of syntax that's evolved haphazardly, with multiple inspirations.
I was particularly amused by this:
For the arrow function, it's kind of implied in the name ARROW
function that we would need a syntax in the lines offn () =>.
It makes me imagine a Computer Science researcher named Dr. Arrow, who
invented a new kind of function, but didn't know how to write it until
their friend pointed out that their name would make a suitable pun.
In reality, I suspect there are at least two lines of origin:
- The key => value syntax for arrays is pretty clearly inspired by
Perl, which was far and away the most popular web programming language
in the early 1990s.
In Perl, it's called the "fat comma", and (a => 'b') is actually a
synonym for ('a', 'b').
- Using some form of "args arrow expression" for lambda functions comes
ultimately, I suspect, from Functional Programming languages. The "=>"
in match statements can probably be traced to there as well.
Notably, exactly what arrow is used varies from language to
language. CoffeeScript (created in 2009) has both "(args) -> expression"
and "(args) => expression" with different semantics;
JavaScript/ECMAScript basically adopted the "=>" version in 2015.
Meanwhile, C# added "(args) => expression" in 2007; Java added "(args)
-> expression" in 2014. Hack used "(args) ==> expression" but needed an
ugly parser implementation, and an early PHP proposal for "(args) ~>
expression" was rejected, so we ended up with "fn(args) => expression"
instead.
Ultimately, the thing that all the uses of "=>" have in common is some
reason to want an ASCII representation of an arrow, and the need to fit
it into the constrained space of an existing grammar.
The same is basically true of "as" - it's a nice short English word,
with multiple meanings. That's a blessing and a curse: a blessing,
because we can reuse it in different contexts without reserving more
words; a curse, because people might have different intuitions about
which meaning was intended.
Which brings me back to where I started this e-mail: my conclusion is
that neither "as" nor "=>" is clear and unambiguous. We could probably
pick either, and people would get used to it; or we could try for
something better.
Regards,
--
Rowan Tommins
[IMSoP]
On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
More updates to Context Managers:
- We have added "masking" for the context variable, using essentially
the same technique as the block scope RFC.- We have added support for
try using, as a shorthand for when you
want to wrap a try-catch-finally around a using statement anyway.More details of both are in the RFC.
As no one seems to have a strong opinion on
continue, we will most
likely proceed with the current approach of matchingswitchbehavior.There doesn't seem to be much interest in making
usingan expression,
which I find unfortunate, but that means we'll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the waythrowwas changed).--Larry Garfield
Since the only feedback on what to use for "as" was that => makes sense, we have changed the RFC to use => instead. So the new syntax is
using (new CM() => $cVar) {
// Do stuff here.
}--Larry Garfield
I’d ask you to get back to “use” keyword, despite of it’s in use in Laravel or somewhere else.
As a developer I cannot even imagine what “use” could mean in web frameworks context, I hope it could have a better name and at the same time we can advise to use namespaces if you don’t want to get something broken after upgrading language.
Just my 5 cents.
--
Best regards,
Dmitrii Derepko.
@xepozz
On Dec 16, 2025, at 1:19 AM, Larry Garfield larry@garfieldtech.com
wrote:On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:
Arnaud and I would like to present another RFC for consideration:
Context Managers.https://wiki.php.net/rfc/context-managers
You'll probably note that is very similar to the recent proposal from
Tim and Seifeddine. Both proposals grew out of casual discussion
several months ago; I don't believe either team was aware that the
other was also actively working on such a proposal, so we now have two.
C'est la vie. :-)Naturally, Arnaud and I feel that our approach is the better one. In
particular, as Arnaud noted in an earlier reply, __destruct() is
unreliable if timing matters. It also does not allow differentiating
between a success or failure exit condition, which for many use cases
is absolutely mandatory (as shown in the examples in the context
manager RFC).The Context Manager proposal is a near direct port of Python's
approach, which is generally very well thought-out. However, there are
a few open questions as listed in the RFC that we are seeking feedback
on.Discuss. :-)
More updates to Context Managers:
- We have added "masking" for the context variable, using essentially
the same technique as the block scope RFC.- We have added support for
try using, as a shorthand for when you
want to wrap a try-catch-finally around a using statement anyway.More details of both are in the RFC.
As no one seems to have a strong opinion on
continue, we will most
likely proceed with the current approach of matchingswitchbehavior.There doesn't seem to be much interest in making
usingan expression,
which I find unfortunate, but that means we'll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the waythrowwas changed).--Larry Garfield
Since the only feedback on what to use for "as" was that => makes sense,
we have changed the RFC to use => instead. So the new syntax isusing (new CM() => $cVar) {
// Do stuff here.
}--Larry Garfield
I’d ask you to get back to “use” keyword, despite of it’s in use in
Laravel or somewhere else.
As a developer I cannot even imagine what “use” could mean in web
frameworks context, I hope it could have a better name and at the same time
we can advise to use namespaces if you don’t want to get something broken
after upgrading language.
Just my 5 cents.--
Best regards,
Dmitrii Derepko.
@xepozz
I have considered asking the same thing in this thread. I have used Laravel
everyday for the last decade and I know how Laravel with helper works. The
reason I decided not to voice this out is because Larry said that even if
Laravel used namespaces it would still not work because the word would be
reserved.
I’m sure it wouldn’t be the end of the world to refactor billions of lines
of code across the industry for a namespace, but for an entire rename of
the function I guess we’re between a rock and a hard place anyway. Plus, I
kind of got used to using fairly quickly.