[RFC] Context Managers

php.internals

Larry Garfield

300 days ago
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

Edmond Dantes

300 days ago
Hello. Thank you for the RFC. An excellent tool for a language that supports interfaces.
-- Ed

Удальцов Валентин

300 days ago
ср, 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. —

Edmond Dantes

300 days ago
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

Paul Dragoonis

300 days ago
On Tue, Nov 4, 2025, 8:18 PM Larry Garfield <larry@garfieldtech.com> 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. :-) >
Great idea, I'm definitely behind this. I've also read through all the PR code. I have a few questions 1. Apart from wrapping zend_resource into ResourceContext, has there been discussions or ideas to wrap other things ? 2. Are there any scenarios where using with() is a bad idea or has "side effects"? 3. 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!

Arnaud.lb

293 days ago
Hi Paul, On Wed, Nov 5, 2025 at 7:30 AM Paul Dragoonis <dragoonis@gmail.com> wrote:
> 3. 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

Paul Dragoonis

293 days ago
On Wed, Nov 12, 2025, 4:30 PM Arnaud Le Blanc <arnaud.lb@gmail.com> wrote:
> Hi Paul, > > On Wed, Nov 5, 2025 at 7:30 AM Paul Dragoonis <dragoonis@gmail.com> wrote: > > 3. 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. >
Hi Arnaud, Ok that answers it, thanks.

Deleu

300 days ago
On Tue, 4 Nov 2025 at 17:18 Larry Garfield <larry@garfieldtech.com> 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. :-) > > -- > 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

Juris Evertovskis

299 days ago
On 2025-11-05 09:38, Deleu wrote:
> 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

Davey Shafik

300 days ago
> On Nov 4, 2025, at 12:18, Larry Garfield <larry@garfieldtech.com> 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. :-)
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

Niels Dossche

299 days ago
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

Larry Garfield

299 days ago
On Tue, Nov 4, 2025, at 2:13 PM, 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. :-)
Hi all. I'm going to reply to several people at once in a single message, for simplicity: On Wed, Nov 5, 2025, at 12:29 AM, Paul Dragoonis wrote:
> 1. 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.)
> 2. 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.
> 3. 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. On Wed, Nov 5, 2025, at 12:45 AM, Valentin Udaltsov wrote:
> 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. On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote:
> 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. :-) On Wed, Nov 5, 2025, at 3:25 AM, Davey Shafik wrote:
> 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: 1. 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. 2. 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: 1. Java uses a parenthetical block on `try` for 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. 2. Either `use` or `using`. The semantics here would be identical to the current `with` proposal. 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

Rowan Tommins [IMSoP]

299 days ago
On 05/11/2025 22:37, Larry Garfield wrote:
> `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.
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]

Derick Rethans

294 days ago
On Wed, 5 Nov 2025, Larry Garfield wrote:
> On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote: > > > 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 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.
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.
> 1. Java uses a parenthetical block on `try` for 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.
> 2. Either `use` or `using`. The semantics here would be identical to > the current `with` proposal.
IMO, the try syntax is more confusing than another overload of use. cheers, Derick

Larry Garfield

293 days ago
On Tue, Nov 11, 2025, at 5:31 AM, Derick Rethans wrote:
> On Wed, 5 Nov 2025, Larry Garfield wrote: > >> On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote: >> >> > 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 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. > > 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.
>> 1. Java uses a parenthetical block on `try` for 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. > > … > >> 2. Either `use` or `using`. The semantics here would be identical to >> the current `with` proposal. > > IMO, the try syntax is more confusing than another overload of use. > > cheers, > Derick
How about `using`? --Larry Garfield

Arnaud.lb

293 days ago
Hi Derick,
> > On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote: > > > > > 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

Bob Weinand

299 days ago
Hey Larry, Tim, Seifeddine and Arnauld, On 4.11.2025 21:13:18, 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. :-)
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

Rowan Tommins [IMSoP]

299 days ago
On 05/11/2025 22:38, Bob Weinand wrote:
> 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]

Bob Weinand

299 days ago
Hey Rowan, On 6.11.2025 00:27:41, Rowan Tommins [IMSoP] wrote:
> On 05/11/2025 22:38, Bob Weinand wrote: >> 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

Rowan Tommins [IMSoP]

299 days ago
On 5 November 2025 23:58:52 GMT, Bob Weinand <bobwei9@hotmail.com> wrote:
>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]

Tim Düsterhus

277 days ago
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

Larry Garfield

296 days ago
On Wed, Nov 5, 2025, at 5:58 PM, Bob Weinand wrote:
> 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

Tim Düsterhus

274 days ago
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() > 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."
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

Rowan Tommins [IMSoP]

274 days ago
On 01/12/2025 14:26, Tim Düsterhus wrote:
> 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()).
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]

Larry Garfield

273 days ago
On Mon, Dec 1, 2025, at 9:38 AM, Rowan Tommins [IMSoP] wrote:
> On 01/12/2025 14:26, Tim Düsterhus wrote: >> 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()). > > > 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.
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

Jeffrey Dafoe

299 days ago
> 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

Tim Düsterhus

277 days ago
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

Rowan Tommins [IMSoP]

296 days ago
On 05/11/2025 22:38, Bob Weinand wrote:
> 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]

Larry Garfield

296 days ago
On Sat, Nov 8, 2025, at 1:54 PM, Rowan Tommins [IMSoP] wrote:
> On 05/11/2025 22:38, Bob Weinand wrote: >> 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

Seifeddine Gmati

295 days ago
On Wed, 5 Nov 2025 at 23:39, Bob Weinand <bobwei9@hotmail.com> wrote:
> > Hey Larry, Tim, Seifeddine and Arnauld, > > On 4.11.2025 21:13:18, 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. :-) > > 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: 1. A **`Resource`** marker interface: This would tell the engine to handle the object specially (e.g., use weak references in backtraces to prevent accidental refcount inflation). 2. A **`Disposable`** interface: This would have the `dispose()` logic and could (and probably should) also be a `Resource`. 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: ```php 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 `Resource`s. 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.

Rowan Tommins [IMSoP]

293 days ago
On 09/11/2025 21:46, Seifeddine Gmati wrote:
> 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).
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 > `ContextManager` design. As discussed in the `use()` thread, we could > later introduce: > > 1. A **`Resource`** marker interface: This would tell the engine to > handle the object specially (e.g., use weak references in backtraces > to prevent accidental refcount inflation). > > 2. A **`Disposable`** interface: This would have the `dispose()` > logic and could (and probably should) also be a `Resource`.
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 `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:
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 for `Resource`s. 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.
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: 1. It is still the open resource from get_resource(); the call succeeds, but the resource has leaked 2. It is a closed resource, and the call to doSomething() throws an "object already disposed" Error 3. 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 initial `use()` 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]

Edmond Dantes

295 days ago
Hello all.
> It makes it very easy to not call the exitContext() method when calling > enterContext() manually
Consider the code below: ```php 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? ```php ... 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? ```php ... 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 like `fopen` and `fclose` should 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

Rowan Tommins [IMSoP]

299 days ago
On 04/11/2025 20:13, Larry Garfield wrote:
> Arnaud and I would like to present another RFC for consideration: Context Managers. > > https://wiki.php.net/rfc/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]

Larry Garfield

279 days ago
On Tue, Nov 4, 2025, at 2:13 PM, 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. :-) > > -- > Larry Garfield > larry@garfieldtech.com
I didn't mention this explicitly, so I'll do that now: * We have changed the keyword from `with` to `using`, 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 `using` into an expression rather than a statement, so that it can be used in expression contexts? * Does anyone want to argue about how `continue` should behave? Right now it matches `switch`, 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

Weedpacket

278 days ago
On 2025-11-27 06:48, Larry Garfield wrote:
> > * Is there any interest in turning `using` into 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

Larry Garfield

271 days ago
On Tue, Nov 4, 2025, at 2:13 PM, 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 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

Larry Garfield

259 days ago
On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:
> On Tue, Nov 4, 2025, at 2:13 PM, 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 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
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

Matthew Weier O'Phinney

259 days ago
On Mon, Dec 15, 2025, 3:19 PM Larry Garfield <larry@garfieldtech.com> wrote:
> On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote: > > On Tue, Nov 4, 2025, at 2:13 PM, 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 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 > > 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.
-- Matthew Weier O'Phinney mweierophinney@gmail.com https://mwop.net/ he/him

Ben Ramsey

259 days ago

Deleu

259 days ago
On Mon, 15 Dec 2025 at 20:01 Ben Ramsey <ben@benramsey.com> wrote:
> >> 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, > 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.

Ben Ramsey

259 days ago
> On Dec 15, 2025, at 18:21, Deleu <deleugyn@gmail.com> wrote: > > On Mon, 15 Dec 2025 at 20:01 Ben Ramsey <ben@benramsey.com> wrote: >>> >>> >>> 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, >> 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.
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

Rowan Tommins [IMSoP]

259 days ago
(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]

Tim Düsterhus

259 days ago
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

Rowan Tommins [IMSoP]

258 days ago
On 16/12/2025 09:07, Tim Düsterhus wrote:
> 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]

Weedpacket

259 days ago
On 2025-12-16 21:08, Rowan Tommins [IMSoP] wrote:
> > 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".

Deleu

259 days ago
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 as `for context`, the only difference that makes me like this one more is because `for context` doesn't sit well with `try context` while `for using` could go well with `try 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

Larry Garfield

259 days ago
On Tue, Dec 16, 2025, at 5:25 AM, Deleu 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 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.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 as `for context`, the only difference that makes me > like this one more is because `for context` doesn't sit well with `try > context` while `for using` could go well with `try 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
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

Deleu

259 days ago
> > 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

Matthew Weier O'Phinney

258 days ago
On Tue, Dec 16, 2025 at 10:01 AM Larry Garfield <larry@garfieldtech.com> wrote:
> On Tue, Dec 16, 2025, at 5:25 AM, Deleu 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 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.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 as `for context`, the only difference that makes me > > like this one more is because `for context` doesn't sit well with `try > > context` while `for using` could go well with `try 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. > > > 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

Rowan Tommins [IMSoP]

258 days ago
On 16/12/2025 11:25, Deleu wrote:
> My conclusion here is exactly the opposite of yours because this > explanation makes `as` even 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 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.
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. On 16/12/2025 15:59, Larry Garfield wrote:
> 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 of `fn () =>`.
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: 1) 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'). 2) 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]

Dmitry Derepko

259 days ago
> 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: >>> On Tue, Nov 4, 2025, at 2:13 PM, 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 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 > > 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

Deleu

259 days ago
On Tue, 16 Dec 2025 at 02:58 Dmitry Derepko <xepozzd@gmail.com> wrote:
> > > > 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: > >>> On Tue, Nov 4, 2025, at 2:13 PM, 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 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 > > > > 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
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.

Larry Garfield

230 days ago
On Tue, Nov 4, 2025, at 2:13 PM, 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. :-)
Hi folks. The holidays are over, so we're back on Context Managers. There are three questions outstanding, 2 of which we want feedback on before we can finalize for a vote. 1. Discussing between us, Arnaud and I aren't confident in expression-based `using`. It would necessitate a trailing semicolon in all cases, and while we have ideas for the return value there's nothing that has clear and indisputable benefit. So we're going to drop this one unless anyone wants to make a strong case for it. 2. The syntax bikeshed. Right now, what we have in the RFC is `using (new CM() => $var)`. We feel that's sufficiently self-descriptive for "produces". However, there was some pushback on that and we're still open to other ideas here. The critera would be "clear, unambiguous, and easy to type". If there's no clear consensus, we'll probably stick with `=>`. 3. One idea we've discussed internally is allowing non-context-manager objects in the `using` statement. If the left-side value in `using` is a non-CM, then it will itself be used as the context variable. It would still get unset at the end of the block, so if simply unsetting it is sufficient cleanup it would eliminate the need for a wrapper. This would remove the need for special casing of `resource` variables, and would also, in effect, absorb the behavior of the block scoping `let` proposal. The block scoping behavior becomes a degenerate case of `using`, feeding two birds with one bird feeder. (To be more animal friendly.) I'm open to that idea, though Arnaud doesn't like it on the grounds that it could be too confusing for folks. So we're putting it out to see if there is a consensus on it. Once those issues are addressed, I think we're nearly able to take CMs to a vote. (If anyone else wants to weigh in on some other part as well, even if it's just a voice of support/approval, now is the time.) Cheers. --Larry Garfield

Tim Düsterhus

229 days ago
Hi Note: I've not been able to fully work through the ML discussion and meaningfully think about all the commentary. This includes some of the older emails. I have also not yet given the RFC an in-depth read, like I would have normally done. Some (incomplete) notes for now: Am 2026-01-13 23:19, schrieb Larry Garfield:
> Once those issues are addressed, I think we're nearly able to take CMs > to a vote. (If anyone else wants to weigh in on some other part as > well, even if it's just a voice of support/approval, now is the time.)
Something that I had noted in response to Rowan in the (what you call) “Bonus Thread” in https://news-web.php.net/php.internals/129582 and also in this RFC's discussion in https://news-web.php.net/php.internals/129618 and what was also mentioned by Marco Deleu in https://news-web.php.net/php.internals/129083 and what I feel is neither properly justified within the RFC text and as far as I can tell was not really discussed either: The fact that `break;` and `continue;` target `using()` blocks. To me it violates the principle of least surprise that foreach ($users as $user) { using (new SuppressErrors()) { if ($user->isAdmin()) { $firstAdmin = $user; break; } } } is incorrect code. The RFC just states *that* this will happen as a fact, but does not attempt to explain *why* that decision was made and neither are there any examples showcasing how that could be useful. I'd also like to note that there's also an “Open Issue” listed in the RFC that is related to this topic. --------------- With regard to the desugaring listed at the top: Can you please also provide the desugaring for the case where no context variable is specified for completeness? Best regards Tim Düsterhus

Larry Garfield

229 days ago
On Thu, Jan 15, 2026, at 9:52 AM, Tim Düsterhus wrote:
> Hi > > Note: I've not been able to fully work through the ML discussion and > meaningfully think about all the commentary. This includes some of the > older emails. I have also not yet given the RFC an in-depth read, like I > would have normally done. > > Some (incomplete) notes for now: > > Am 2026-01-13 23:19, schrieb Larry Garfield: >> Once those issues are addressed, I think we're nearly able to take CMs >> to a vote. (If anyone else wants to weigh in on some other part as >> well, even if it's just a voice of support/approval, now is the time.) > > Something that I had noted in response to Rowan in the (what you call) > “Bonus Thread” in https://news-web.php.net/php.internals/129582 and also > in this RFC's discussion in > https://news-web.php.net/php.internals/129618 and what was also > mentioned by Marco Deleu in > https://news-web.php.net/php.internals/129083 and what I feel is neither > properly justified within the RFC text and as far as I can tell was not > really discussed either: > > The fact that `break;` and `continue;` target `using()` blocks. To me it > violates the principle of least surprise that > > foreach ($users as $user) { > using (new SuppressErrors()) { > if ($user->isAdmin()) { > $firstAdmin = $user; > break; > } > } > } > > is incorrect code. The RFC just states *that* this will happen as a > fact, but does not attempt to explain *why* that decision was made and > neither are there any examples showcasing how that could be useful.
The core point here is that we needed some way to successfully terminate the block early, and `return` was already taken by the function. `break` seemed like the obvious keyword to use, as in other contexts it also means "skip to the end of this block." `continue` is really just to mirror what switch does with it. If the consensus is to simply disallow `continue` entirely (which is inconsistent with what switch does today), we're OK with that. I don't feel strongly either way, so we're looking for a consensus. If there's a different keyword than `break` you think would make more sense here, please do suggest it and provide an argument. But "break does the same thing here it does in switch, foreach, and while" seems like a pretty straightforward approach.
> I'd also like to note that there's also an “Open Issue” listed in the > RFC that is related to this topic. > > --------------- > > With regard to the desugaring listed at the top: Can you please also > provide the desugaring for the case where no context variable is > specified for completeness?
Desugaring at the top? The desugaring is explained about a third of the way down. :-) Is that what you mean? (In the "Implementation" section.) It doesn't make any sense to go into that level of detail in the introduction. --Larry Garfield

Juris Evertovskis

227 days ago
On 2026-01-15 18:01, Larry Garfield wrote:
> `continue` is really just to mirror what switch does with it. If the > consensus is to simply disallow `continue` entirely (which is > inconsistent > with what switch does today), we're OK with that. I don't feel > strongly > either way, so we're looking for a consensus. >
Hey, I think the benefit of `switch` supporting `continue` is to ensure consistency in the number of breakable/continuable structures. Consider foreach ($a as $e) { switch ($e) { case 0: foreach ($x as $y) { if (!$y) break 3; } } } If I need to change `break` to `continue`, I will naturally change `break 3` to `continue 3`. I would suggest sticking to same in context managers — have it respected not because you would ever `continue` to exit it, but to have the onion layer numbers consistent. BR, Juris

Tim Düsterhus

226 days ago
Hi On 1/15/26 17:01, Larry Garfield wrote:
> The core point here is that we needed some way to successfully terminate the block early
As I asked in my previous email: Why? You state this requirement as if it was an undisputable fact, but I don't see how it is *needed* and no reasoning is given.
> `continue` is really just to mirror what switch does with it. If the consensus is to simply disallow `continue` entirely (which is inconsistent with what switch does today), we're OK with that. I don't feel strongly either way, so we're looking for a consensus.
What do you mean by “disallow `continue` entirely”? Does this include `continue 2;` targeting a loop or is this just referring to “continue targeting `using()`?
> If there's a different keyword than `break` you think would make more sense here, please do suggest it and provide an argument. But "break does the same thing here it does in switch, foreach, and while" seems like a pretty straightforward approach.
“break does a different thing here than it does in if, else, try, catch, finally, namespace, and declare” doesn't seem like a straightforward approach to me. To me using() feels much closer to an if() (or try, given the desugaring) than to a loop. As I had mentioned in my email https://news-web.php.net/php.internals/129582, `goto` *is fine*. And `do-while(false)` would also work as a restricted form of “forward goto”.
>> With regard to the desugaring listed at the top: Can you please also >> provide the desugaring for the case where no context variable is >> specified for completeness? > > Desugaring at the top? The desugaring is explained about a third of the way down. :-) Is that what you mean? (In the "Implementation" section.) It doesn't make any sense to go into that level of detail in the introduction.
Yes, I meant the first code block in the “Implementation” section. To me that felt like the “top” of the RFC. I didn't mean to suggest to move it elsewhere, I requested an example of what: using (new Manager()) without the `=> $var` results in. Best regards Tim Düsterhus

Tim Düsterhus

224 days ago
Hi On 1/13/26 23:19, Larry Garfield wrote:
> Once those issues are addressed, I think we're nearly able to take CMs to a vote. (If anyone else wants to weigh in on some other part as well, even if it's just a voice of support/approval, now is the time.)
Something I noticed while reviewing the block scoping RFC: Both RFCs come with new OPcodes in the engine. This can have an impact on extensions that work with OPcodes, for example profilers and debuggers. Block scoping already mentioned this in the RFC Impact section, but the context manager RFC does not. For block scoping the two OPcodes are relatively straight-forward assignments which should (hopefully) be easy to take into account for a debugger. For context managers there seems to be more associated logic to set up the scope within the ZEND_INIT_USING OPcode, which might be more troublesome for debuggers to correctly reason about when stepping through the code (e.g. with the $__RETURN_VALUE meta variable or whatever it is called). Perhaps Derick can provide insight here? Looking at that OPcode I'm also seeing the initialization of the `ResourceContextManager` class. In the RFC it is called `ResourceContext` and it is non-final there (final in the implementation). This is an inconsistency that should be fixed. For that one I was also wondering if it is possible to directly initialize it in userland (the constructor seems to be public) and if it will then behave as expected. I assume the answer is yes to both, but it would be useful for the RFC to clarify this. Best regards Tim Düsterhus

Claude Pache

222 days ago
> Le 13 janv. 2026 à 23:19, Larry Garfield <larry@garfieldtech.com> a écrit : > > On Tue, Nov 4, 2025, at 2:13 PM, 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. :-) > > Hi folks. The holidays are over, so we're back on Context Managers. > > [...] > > > --Larry Garfield
Hi, Just a small question. What happens when an `exit`/`die` instruction is executed inside a `using` block? Is the relevant `exitContext()` handler invoked, just like for an early `return` or `break`? This is probably self-evident, but it is worth to state it explicitly, because, for some hysterical reason, relevant `finally` blocks are *not* executed with `exit`. —Claude

Larry Garfield

222 days ago
On Wed, Jan 21, 2026, at 1:01 PM, Claude Pache wrote:
>> Le 13 janv. 2026 à 23:19, Larry Garfield <larry@garfieldtech.com> a écrit : >> >> On Tue, Nov 4, 2025, at 2:13 PM, 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. :-) >> >> Hi folks. The holidays are over, so we're back on Context Managers. >> >> [...] >> >> >> --Larry Garfield > > > Hi, > > Just a small question. What happens when an `exit`/`die` instruction is > executed inside a `using` block? Is the relevant `exitContext()` > handler invoked, just like for an early `return` or `break`? > > This is probably self-evident, but it is worth to state it explicitly, > because, for some hysterical reason, relevant `finally` blocks are > *not* executed with `exit`. > > > —Claude
At runtime, it's "just" a finally block, so it would behave the same. Which I agree is absurd, but this is PHP after all... --Larry Garfield

Volker Dusch

229 days ago
On Tue, Nov 4, 2025 at 9:19 PM Larry Garfield <larry@garfieldtech.com> 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. :-) > > -- > Larry Garfield > larry@garfieldtech.com >
Hi Larry, Hi Internals, Last year I promised you (Larry) some feedback on-list as well and didn't get around to it until now. I recognize the strain that repeating arguments has on a discussion like this, and this topic has already taken up a lot of focus and time for the folks here. But I wanted to at least explain why I think PHP would be better off without having this in core and why I think it would be a net negative for PHP to have this. So to summarize, I find the feature doesn't fit in PHP. It's introducing more magically called methods, burdened with unnecessary complexity, while being very limited in its potential (sensible) uses. Combined with its class-only high-verbosity approach, I feel this is lacking places where it would improve PHP code in general and better suited for a library for people that want this type of, what I consider, magical indirection. With the name also being extremely generic and non-descriptive, this all feels like bloat to me that complicates the language for no tangible benefits. To expand a bit on the points: - Non-local behavior: Every using statement is a couple of function calls that are non-obvious in how they delegate to some __magic interface methods that are not supposed to (but very able to) be called explicitly. With the implicit catch and exitContext() ability to return true/false; to rethrow/suppress an exception, adding even more hidden branching to execution. - Variable masking: A new block masking and restoring the context variables but not others is an additional source of errors and confusion that I feel doesn't pay off in terms of value vs. added complexity and error sources. It's not behavior we have anywhere else in PHP and breaks the flow of reading and reasoning about code in non-obvious ways. - Break/Continue semantics: There is no clear reason for me why this block scope should allow early returns. If the content is growing to a point where it's needed, a function is already a reasonable scope. Given that PHP allows for `break 2;`, something that we'll see more of then, it's manageable. It just adds to the, for me, unreasonable complexity of the feature. - Naming: For me, despite having worked with Python, the name means absolutely nothing. It doesn't even manage the context of the invocation. If anything, it manages when a resource is released into and removed and deallocated from a scoped context. And that sentence also is rough. PHP, for better or worse, doesn't burden its users with having to study many CS concepts beyond basic OO or procedural programming and still allows them to write obvious, valuable, and predictable code. I understand that with its evolution this has changed, and we have added a lot of redundancy (short arrays, short functions, pipes, etc..) to provide sugar that has steepened the learning curve for some. Adding very specific single-use concepts to the language with their own disconnected naming schemes, syntax, or, in this case, hidden behaviors should be carefully considered. And while I'm sure you did we came to different conclusions. - Block scoping: Personally, I don't see the need for block scoping in PHP in general. But having a generic solution that works without creating a new class for each case would feel like something that at least can be used by everyone and every part of the language. Tying this to custom objects doesn't feel like a language level feature but something that should be in a library. The worst option would be to allow using() to take a context manager or a plain expression and make people guess every time the statement is used if hidden function calls are attached to it. - Verbosity: Having to implement three code paths for each ContextManager (enter, exitWithoutError, exitWithError) within two functions, with a near mandatory `if` in a separate class, doesn't strike me as useful over patterns like getting and returning a connection out of a pool “manually.” The trade-off between this and already existing solutions to this problem with try/finally or construct/destruct isn't enticing. - Object lifecycle in PHP: Just to reiterate because it bugs me as PHP zval life cycles are used as an argument here: Reference counting in PHP is fully deterministic, and code like `function () { $x = new Foo(); return; }` will deterministically construct and destruct (at the end of the function as the variable gets cleaned up). Use cases where the GC would actually come into play are extremely rare from the real-world usages we can see in Python. I also haven't seen an example in PHP nor something in the RFC that looks overly convincing in improving this with managed in-function unsets. The error handling option is nice, but for maintainability, simplicity, and effort in writing code, I'd still prefer this to try/(catch)/finally Layering another level of lifecycle management on top of the existing PHP behavior doesn't feel like a simplification but rather like another source of complexity with this new niece special case.
-- In summary, this feels like beyond what's necessary to get rid of a couple of try/finally blocks per application and encourages bad patterns like using ContextMangers for async instead of more modern APIs that have evolved since then. Kind Regards, Volker

Tim Düsterhus

226 days ago
Hi On 1/15/26 19:38, Volker Dusch wrote:
> a function is already a reasonable scope
That - together with the other points you raised - made me realize one thing: In contrast to block scoping, the main purpose of “Context Managers” is *not* managing arbitrary variables in the current scope in combination with existing control structures. Instead it is just managing a single variable in a reasonably *self-contained* fashion, with other variables just needing to “exist”. It could therefore also just be a regular function taking a callback, as is already done in userland, e.g. with Laravel's DB::transaction() helper. I believe something like this would be equivalent to the RFC: function using( callable $runInContext, ContextManager ...$managers ): void { $contexts = []; foreach ($managers as $manager) { $contexts[] = $manager->enterContext(); } try { $runInContext(...$contexts); foreach (array_reverse($managers) as $manager) { $manager->exitContext(); } } catch (Throwable $e) { foreach (array_reverse($managers) as $manager) { if ($manager->exitContext($e) === true) { $e = null; } } if ($e !== null) { throw $e; } } } This would completely side-steps the “break to exit” question, since a `return` will just work. It also avoids introducing new keywords, it just requires a new function in the global namespace. I understand that using variables from the current scope in a Closure is currently not particularly convenient, particularly when they need to be changed. However this is something that can be solved in a generic fashion, for example inspired by the C++ lambda syntax: // Captures everything by value and $result by reference. function ($context) use (*, &$result) { } // Captures everything by reference. function ($context) use (&*) { } which would also be useful in other situations. Best regards Tim Düsterhus

Larry Garfield

225 days ago
On Thu, Jan 15, 2026, at 12:38 PM, Volker Dusch wrote:
> Hi Larry, Hi Internals, > > Last year I promised you (Larry) some feedback on-list as well and > didn't get around to it until now. I recognize the strain that > repeating arguments has on a discussion like this, and this topic has > already taken up a lot of focus and time for the folks here. > > But I wanted to at least explain why I think PHP would be better off > without having this in core and why I think it would be a net negative > for PHP to have this. > > So to summarize, I find the feature doesn't fit in PHP. It's > introducing more magically called methods, burdened with unnecessary > complexity, while being very limited in its potential (sensible) uses. > Combined with its class-only high-verbosity approach, I feel this is > lacking places where it would improve PHP code in general and better > suited for a library for people that want this type of, what I > consider, magical indirection.
As noted in Future Scope, we can add function-based context managers as well based on generators. At the moment we're not convinced it's necessary, but it's a straightforward add-on if we find that always writing a class for a context manager is too cumbersome. The issue with punting this behavior to user-space is that a library cannot provide this sort of functionality in a clean way. In an ideal world, if we had auto-capturing long-closures, then I would agree this is largely unnecessary and could instead be implemented like so (to reuse the examples from the RFC): $conn->inTransaction(function () { // SQL stuff. }); $locker->lock('file.txt', function () { // File stuff. }); $scope->inScope(function () { $scope->spawn(yadda yadda); }); $errorHandlerScope->run(fn() => null, function () { // Do stuff here with no error handling. }); And so forth. If we had auto-capturing closures, I would probably argue that is a better approach. However, auto-capturing closures have been rejected several times, and I have no confidence that we will ever get them. (Whether you approve or disapprove of that is your personal opinion.) The current alternative involves using lots of `use` clauses, which is needlessly clunky to the point that folks try to avoid it. I literally have code like this in a project right now, and I've had to do this many times: public function parseFolder(PhysicalPath $physicalPath, LogicalPath $logicalPath, array $mounts): bool { return $this->cache->inTransaction(function() use ($physicalPath, $logicalPath, $mounts) { // Lots of SQL updates here. }); } That's just gross. :-) This is exactly the example that's been used in the past to argue in favor of auto-capturing closures, but it's never been successful. So given the choices we have made to limit the language, context managers become the next logical option to encapsulate common error handling patterns. We chose to pursue this syntax now because of the ongoing async discussions, as IMO, full structured-only concurrency is the Right Way forward. So rather than a one-off for async, it's better to have a generic syntax that would work for a dozen use cases, not just one. As to it being too "magical," the definition of that is, as always, highly subjective. Magic is just code I don't understand. It should be noted that what is proposed here is almost identical in design to Python, which makes heavy use of this design and is widely regarded as one of the easiest languages to learn. So it's clearly not too magical for beginners.
> With the name also being extremely generic and non-descriptive, this > all feels like bloat to me that complicates the language for no > tangible benefits.
The name was borrowed from Python, which should make it easily understandable for all of those beginners who started on Python. If there's a better name for the Interface you'd like to use, though, we're open to suggestions. Similar (if less robust) functionality exists in C#, also called Context Managers (https://useful.codes/working-with-context-managers-in-c-sharp/). Context Managers are also used in Java, again for similar but not as robust functionality (https://useful.codes/working-with-context-managers-in-java/). We modeled on Python, as it was the most robust of the existing options, but "context manager" does seem to be the de facto standard name for this pattern.
> To expand a bit on the points: > > - Non-local behavior: > > Every using statement is a couple of function calls that are > non-obvious in how they delegate to some __magic interface methods > that are not supposed to (but very able to) be called explicitly. With > the implicit catch and exitContext() ability to return true/false; to > rethrow/suppress an exception, adding even more hidden branching to > execution.
__construct, __destruct, __get, property hooks, ArrayAccess, Iterable, __serialize, ... PHP decided that triggering "hidden" behavior at certain points was acceptable decades ago. If anything, the use of a dedicated keyword here makes it less magical than __get or property hooks, as it clues the reader in that a context manager is being used. And in every one of those cases, it's technically possible to call the magic or interface method explicitly, but it's culturally discouraged. There's no reason Context Managers should not be in the exact same category. If we could use auto-capturing closures, it would effectively just be the strategy pattern. But as above, PHP has decided that we need a few more steps to make that work, which Context Managers resolve.
> - Variable masking: > > A new block masking and restoring the context variables but not others > is an additional source of errors and confusion that I feel doesn't > pay off in terms of value vs. added complexity and error sources. > > It's not behavior we have anywhere else in PHP and breaks the flow of > reading and reasoning about code in non-obvious ways.
This was added largely because it was requested for the block scoping RFC, and it seemed to make sense here too. If the consensus is that it's not worth it, we're OK with pulling that part back out. It's not core functionality. Does anyone else feel strongly either way on this point?
> - Break/Continue semantics: > > There is no clear reason for me why this block scope should allow > early returns. If the content is growing to a point where it's needed, > a function is already a reasonable scope. Given that PHP allows for > `break 2;`, something that we'll see more of then, it's manageable. It > just adds to the, for me, unreasonable complexity of the feature.
Even a 4 line block could have an if statement in it, which may involve terminating the block early without throwing an exception. Without `break` or similar, the only option for the user would be a `goto` and a label after the `using` block ends. I hope we don't need to explain why that is an inferior solution. As far as an example (to Tim's email): function ensure_header(string $filename, array $header) { using (open_file($filename) as $f) { $first_line = fgets($f); $first = parse_csv($first_line); if ($first[0] === $header[0]) { break; // The thing we want to ensure is already the case, so bail out. } // Logic here to prepend $header to the file. } } While it would be possible to reverse the conditional, almost every modern recommendation is to use early returns as much as possible. Or in this case early `break`.
> - Naming: > > For me, despite having worked with Python, the name means absolutely > nothing. It doesn't even manage the context of the invocation. If > anything, it manages when a resource is released into and removed and > deallocated from a scoped context. And that sentence also is rough. > > PHP, for better or worse, doesn't burden its users with having to > study many CS concepts beyond basic OO or procedural programming and > still allows them to write obvious, valuable, and predictable code. I > understand that with its evolution this has changed, and we have added > a lot of redundancy (short arrays, short functions, pipes, etc..) to > provide sugar that has steepened the learning curve for some. > Adding very specific single-use concepts to the language with their > own disconnected naming schemes, syntax, or, in this case, hidden > behaviors should be carefully considered. And while I'm sure you did > we came to different conclusions.
Again, I go back to the example of Python, often lauded as a great beginner language. It lets you write procedural or OOP (though it does have multiple inheritance), though it's weaker on functional than PHP 8.6 will be, I'd argue. But it makes heavy use of context managers, and it doesn't seem to hurt anyone. As far as redundancy, that's in large part because PHP was never designed, it's just been patched over the last 30 years. But often, we're just moving up the abstraction curve along with the rest of the language community. Or identifying common patterns and problems and finding ways to extract out the hard bits to make them easier. CSS, incidentally, evolves the same way: Find common patterns and problems, figure out a general language-level solution, and add new features to the language to turn "500 lines of Javascript" into "2 CSS keywords." Remember, all code is syntactic sugar over assembly. :-)
> - Block scoping: > > Personally, I don't see the need for block scoping in PHP in general. > But having a generic solution that works without creating a new class > for each case would feel like something that at least can be used by > everyone and every part of the language.
I disagree that "everyone" will have to write a CM. In practice, I'd expect most people wouldn't; it would be part of the API exposed by library X, and users of that library will use the CMs that are provided. The whole point is that the logic is reusable, and thus reduces the need for "everyone" to write it. For example, Doctrine could provide a single InTransaction CM, which every single user of Doctrine would benefit from. (Much the same as Doctrine's existing inTransaction() method, which suffers from the use-bloat problem described above.) PHP itself could provide a single CM for files, possibly using SplFile, so no one else would have to write one, ever, unless they needed some highly wonky custom logic. In which case they'd be custom writing something anyway. But this way, they get the recommended error handling out-of-the-box in the standard case. As far as a "generic solution," I have added a section to the RFC on "value escape," based on an observation I made a while back in the bonus thread. Specifically, there will *always* be a failure case if the context variable escapes (or its equivalent in traditional code), but there is no universal answer to what failure case you want. A Context Manager approach allows you to explicitly decide that for each situation as needed.
> Tying this to custom objects doesn't feel like a language level > feature but something that should be in a library.
As noted above, PHP has deliberately chosen to make library-based solutions to this space inferior.
> The worst option would be to allow using() to take a context manager > or a plain expression and make people guess every time the statement > is used if hidden function calls are attached to it.
So we'll mark you as a no on having that fallback shorthand, then. :-) Would you rather a rudimentary `UnsetAtEnd` CM be included?
> - Verbosity: > > Having to implement three code paths for each ContextManager (enter, > exitWithoutError, exitWithError) within two functions, with a near > mandatory `if` in a separate class, doesn't strike me as useful over > patterns like getting and returning a connection out of a pool > “manually.” The trade-off between this and already existing solutions > to this problem with try/finally or construct/destruct isn't enticing.
I disagree, naturally. Just from the examples in the RFC, I'd say the resulting code is far cleaner, less redundant, easier to read, and you're less likely to forget error handling. We debated a 2 method vs 3 method solution, that is, splitting exitContext() into exitSuccess() and exitFailure(). The challenge there is that if you have common logic to happen in both cases, you have to duplicate it. Merging them into exitContext(), you have to deal with an if-statement most of the time. Either way is a trade off. Additionally, you may not want anything to happen on one of exitSuccess() or exitFailure(), in which case you'd have an empty method, or else we use magic methods instead of an interface, which we weren't wild about. So no approach is perfect, so we started with the one that Python has already shown is useful and effective. If there's a different way to organize that code that you think would be better, we're open to suggestions.
> - Object lifecycle in PHP: > > Just to reiterate because it bugs me as PHP zval life cycles are used > as an argument here: Reference counting in PHP is fully deterministic, > and code like `function () { $x = new Foo(); return; }` will > deterministically construct and destruct (at the end of the function > as the variable gets cleaned up). Use cases where the GC would > actually come into play are extremely rare from the real-world usages > we can see in Python. I also haven't seen an example in PHP nor > something in the RFC that looks overly convincing in improving this > with managed in-function unsets. The error handling option is nice, > but for maintainability, simplicity, and effort in writing code, I'd > still prefer this to try/(catch)/finally
To reiterate what I said above and in the new section in the RFC: The issue isn't about reference counting determinism at the engine level. The issue is developer A may expect something to happen when an object goes out of scope, but it won't because developer B stashed a copy of it somewhere so it won't actually destruct. That problem is not created by context managers, and it affects the Block Scoping proposal as well. It's an unavoidable fact of basically any language with automatic garbage collection. You can predict when a variable goes out of scope, but you cannot prevent a reference to its value from continuing to exist past when you expect it, thus delaying any on-cleanup behavior beyond when you expect it. What context managers offer is a way to decide what to do with that situation, because, again, there is no globally applicable answer. But this is one reason that relying on destructors is a poor approach if you want cleanup X to happen at point Y: You can't be certain the destructor will be called then, even if the reference counting logic is fully deterministic. On top of that, destructors, as noted, cannot differentiate between success and failure cases, which often require different cleanup. Externalizing that logic out of the value itself (from the context variable to the context manager) allows flexibility in both cases that simply does not exist otherwise without a large amount of code. Python recommends using CMs for files and similar values precisely for this reason, and has essentially the same ref-count-plus-cycle memory model.
> Layering another level of lifecycle management on top of the existing > PHP behavior doesn't feel like a simplification but rather like > another source of complexity with this new niece special case. > > -- > > In summary, this feels like beyond what's necessary to get rid of a > couple of try/finally blocks per application and encourages bad > patterns like using ContextMangers for async instead of more modern > APIs that have evolved since then.
I would argue that context managers for async *is* the more modern API, and creating/canceling/blocking async tasks manually is the legacy, poor approach. --Larry Garfield

Rowan Tommins [IMSoP]

225 days ago
On 19/01/2026 15:58, Larry Garfield wrote:
> In an ideal world, if we had auto-capturing long-closures, then I would agree this is largely unnecessary and could instead be implemented like so (to reuse the examples from the RFC): > > $conn->inTransaction(function () { > // SQL stuff. > }); > > ... > > If we had auto-capturing closures, I would probably argue that is a better approach.
I haven't caught up with the discussion fully, but I want to pick up on this specifically, because I disagree. Inversion of control like this would only be suitable in the general case if we had auto-capture *by reference*. I believe every proposal so far has limited automatic capture to *values only*. Auto-capture by value helps you get values *into* the closure, but does not help get anything back *out*. So, if the code you want to sugar looks like this: ``` try{ $db->beginTransaction(); // ... $newFooId = $db->execute('INSERT INTO Foo ... RETURNING FooId'); $newBarId = $db->execute('INSERT INTO Foo ... RETURNING BarId'); // ... $db->commitTransaction(); } finally{ if( $db->isInTransaction()) { $db->rollbackTransaction(); } } // use $newFooId and $newBarId here ``` Then your options with a capture-by-value closure are either a) list the outputs as manual by-ref captures: ``` $newFooId = $newBarId = null; $db->inTransaction(fn() use (&$newFooId, &$newBarId) { // ... $newFooId = $db->execute('INSERT INTO Foo ... RETURNING FooId'); $newBarId = $db->execute('INSERT INTO Foo ... RETURNING BarId'); // ... }); // use $newFooId and $newBarId here ``` or b) return the outputs, and extract them using array destructuring or similar: ``` [$newFooId, $newBarId] = $db->inTransaction(fn() { // ... $newFooId = $db->execute('INSERT INTO Foo ... RETURNING FooId'); $newBarId = $db->execute('INSERT INTO Foo ... RETURNING BarId'); // ... return [$newFooId, $newBarId]; });// use $newFooId and $newBarId here ``` A Context Manager - or any other syntax based on a code block rather than a full stack frame - instead gives you direct access to the local variables: ``` using($db->transaction()) { // ... $newFooId = $db->execute('INSERT INTO Foo ... RETURNING FooId'); $newBarId = $db->execute('INSERT INTO Foo ... RETURNING BarId'); // ... }// use $newFooId and $newBarId here ``` Even if we had automatic capture by value, I think Context Managers would be a useful proposal to discuss.
-- Rowan Tommins [IMSoP]

Bob Weinand

117 days ago
Hey Larry,
> Am 19.01.2026 um 16:58 schrieb Larry Garfield <larry@garfieldtech.com>: > > As noted in Future Scope, we can add function-based context managers as well based on generators. At the moment we're not convinced it's necessary, but it's a straightforward add-on if we find that always writing a class for a context manager is too cumbersome. > > The issue with punting this behavior to user-space is that a library cannot provide this sort of functionality in a clean way. > > In an ideal world, if we had auto-capturing long-closures, then I would agree this is largely unnecessary and could instead be implemented like so (to reuse the examples from the RFC): > > $conn->inTransaction(function () { > // SQL stuff. > }); > > $locker->lock('file.txt', function () { > // File stuff. > }); > > $scope->inScope(function () { > $scope->spawn(yadda yadda); > }); > > $errorHandlerScope->run(fn() => null, function () { > // Do stuff here with no error handling. > }); > > And so forth. If we had auto-capturing closures, I would probably argue that is a better approach. > > However, auto-capturing closures have been rejected several times, and I have no confidence that we will ever get them. (Whether you approve or disapprove of that is your personal opinion.) The current alternative involves using lots of `use` clauses, which is needlessly clunky to the point that folks try to avoid it. > > I literally have code like this in a project right now, and I've had to do this many times: > > public function parseFolder(PhysicalPath $physicalPath, LogicalPath $logicalPath, array $mounts): bool > { > return $this->cache->inTransaction(function() use ($physicalPath, $logicalPath, $mounts) { > // Lots of SQL updates here. > }); > } > > That's just gross. :-) This is exactly the example that's been used in the past to argue in favor of auto-capturing closures, but it's never been successful.
I fully agree that this is gross. I have just created a comprehensive RFC https://wiki.php.net/rfc/scope-functions to address this underlying problem you describe. It does address quite a few of the main issues people had with trivial auto-capturing Closures which would simply clone the symbol table. I personally really don't like this Context Managers RFC given the apparent complexity it has (only for heavyweight usages basically, library style - you wouldn't just create ContextManager implementing classes ad hoc for everything). Thus, I'd like to ask you to consider my RFC first and give feedback on it, and possibly - obviously only if you think my RFC is a good choice for the language - pause this RFC for as long as mine is under discussion. Thanks, Bob

Larry Garfield

113 days ago
On Wed, May 6, 2026, at 3:15 PM, Bob Weinand wrote:
> Hey Larry, > >> Am 19.01.2026 um 16:58 schrieb Larry Garfield <larry@garfieldtech.com>: >> >> As noted in Future Scope, we can add function-based context managers as well based on generators. At the moment we're not convinced it's necessary, but it's a straightforward add-on if we find that always writing a class for a context manager is too cumbersome. >> >> The issue with punting this behavior to user-space is that a library cannot provide this sort of functionality in a clean way. >> >> In an ideal world, if we had auto-capturing long-closures, then I would agree this is largely unnecessary and could instead be implemented like so (to reuse the examples from the RFC): >> >> $conn->inTransaction(function () { >> // SQL stuff. >> }); >> >> $locker->lock('file.txt', function () { >> // File stuff. >> }); >> >> $scope->inScope(function () { >> $scope->spawn(yadda yadda); >> }); >> >> $errorHandlerScope->run(fn() => null, function () { >> // Do stuff here with no error handling. >> }); >> >> And so forth. If we had auto-capturing closures, I would probably argue that is a better approach. >> >> However, auto-capturing closures have been rejected several times, and I have no confidence that we will ever get them. (Whether you approve or disapprove of that is your personal opinion.) The current alternative involves using lots of `use` clauses, which is needlessly clunky to the point that folks try to avoid it. >> >> I literally have code like this in a project right now, and I've had to do this many times: >> >> public function parseFolder(PhysicalPath $physicalPath, LogicalPath $logicalPath, array $mounts): bool >> { >> return $this->cache->inTransaction(function() use ($physicalPath, $logicalPath, $mounts) { >> // Lots of SQL updates here. >> }); >> } >> >> That's just gross. :-) This is exactly the example that's been used in the past to argue in favor of auto-capturing closures, but it's never been successful. > > I fully agree that this is gross. I have just created a comprehensive > RFC https://wiki.php.net/rfc/scope-functions to address this underlying > problem you describe. > > It does address quite a few of the main issues people had with trivial > auto-capturing Closures which would simply clone the symbol table. > > I personally really don't like this Context Managers RFC given the > apparent complexity it has (only for heavyweight usages basically, > library style - you wouldn't just create ContextManager implementing > classes ad hoc for everything). > > Thus, I'd like to ask you to consider my RFC first and give feedback on > it, and possibly - obviously only if you think my RFC is a good choice > for the language - pause this RFC for as long as mine is under > discussion. > > Thanks, > Bob
Hi Bob. I've looked over your RFC, and have a number of issues with it as a Context Manager alternative. For one, I don't see an indication that this one would fair any better than the past several attempts. (Such things are impossible to predict.) So blocking context managers on it seems unwise. Second, the restrictions around where it can be used or passed feel very arbitrary (I realize there's a reason for them, but as a user, it feels arbitrary and confusing), which is also a problem. Third, I know I said above that auto-capturing closures would likely obviate the need for context managers, but Rowan had a valid point in his earlier reply to me that CMs have the advantage of not creating a new scope (not even visually). That makes the behavior of any newly created variables inside the `using` body self-evident: They survive. Closures have done the opposite for 17 years, for reasons that are also self-evident (it's a different function). Having a special type of closure that "leaks" by design is very confusing and not at all obvious, especially when it's a type of closure that seems like it would be generally useful. Fourth, and related, it's self-evident that the `using` block's body is executed immediately in-place, the same way a for, while, or try body would be. A shared-scope closure doesn't have that clarity, and could be executed at any arbitrary time in the future before the defining function terminates. That makes it, well, somewhat reusable, which is again, rather confusing. Fifth, I disagree that CMs are particularly "heavyweight." A 2-method class is hardly a large lift, and at runtime it's just try-catch blocks. Certainly, they will likely be used many more times than they are written, but that's a feature, not a flaw. It's also true of most of PHP, like, interfaces. It's also not particularly shorter; the DB Transaction example, for instance, is still roughly the same number of lines of code in either approach. Just one uses a class, one uses a method. Rowan's subsequent suggestion of a macro system is... highly interesting, but also a very, very deep rabbit hole that would take a lot of time to run down. So at this time we are going to continue with the Context Manager RFC, though I appreciate your effort in finding alternative approaches. One thing it has suggested to me is that it may be worth exploring the generator-based approach that's currently in future scope, as that may offer a bit more flexibility and suggest solutions to the return/throw question. --Larry Garfield

Larry Garfield

163 days ago
On Tue, Nov 4, 2025, at 2:13 PM, 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. :-) > > -- > Larry Garfield > larry@garfieldtech.com
Hi folks, and welcome back! Arnaud and I have made a number of changes to the RFC that should make it sleaker and more consistent. The notable ones (that impact behavior) are as follows: 1. We went back and forth on the `continue` question several times, before coming to the conclusion that `continue` is a tool for looping structures only. That `switch` also uses it is just `switch` being silly because reasons, and there is no reason `using` must inherit its weirdness. Therefore, `continue` inside a `using` block now means nothing at all. `continue` will ignore it, the same way it ignores an `if` statement. 2. Several people (including us) were uncomfortable with using a boolean return from the exitContext() method. While that is what Python does, it is indeed not self-evident how it works. (Should true mean "true, I'm done" or "true, rethrow"?) We debated using an enum value, but that appeared to be too verbose. Instead, we decided that exitContext() should return ?Throwable, which is the same thing it is passed. In a success case, it is passed null. In a failure case, it is passed a throwable. So it can then return null (meaning "we're done, nothing else to do here") or a throwable, which will then get thrown. Since in most cases an error should be allowed to propagate, it means simply calling `return $exception` at the end of the method will "do the right thing" 95% of the time. Simple and easy and self-documenting. (If there's a reason to wrap and rethrow the exception, do that and return the new exception. Or to swallow the exception and not propagate it, return null.) We believe this concludes the context manager design. We're pretty happy with where it is at this point. Baring any further substantive feedback, we'll open the vote in a little over 2 weeks. --Larry Garfield

Tim Düsterhus

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

Larry Garfield

155 days ago
On Sun, Mar 29, 2026, at 6:14 AM, Tim Düsterhus wrote:
> Larry, > > Am 2026-03-22 19:19, schrieb Larry Garfield: >> Arnaud and I have made a number of changes to the RFC that should make >> it sleaker and more consistent. The notable ones (that impact >> behavior) are as follows: > > “Sleaker” is not a word my dictionary understands. Was this a typo for > “sleeker” in the sense of “refined”?
Yes, typo. Sleeker in the sense of "fewer bumpy parts on it."
>> 1. We went back and forth on the `continue` question several times, >> before coming to the conclusion that `continue` is a tool for looping >> structures only. That `switch` also uses it is just `switch` being >> silly because reasons, and there is no reason `using` must inherit its >> weirdness. Therefore, `continue` inside a `using` block now means >> nothing at all. `continue` will ignore it, the same way it ignores an >> `if` statement. > > I fail to see how “making break; and continue; behave inconsistently” is > making the RFC (and by extension) the language any more consistent. In > this following example snippet it's not at all obvious that the `break` > is behaving incorrectly by targeting the `using()` with `continue` > targeting the `foreach()`, despite both using the same “number”: > > $processed = 0; > foreach ($entries as $entry) { > using ($db->transaction()) { > switch ($entry['type']) { > case 'EOF': > break 2; > default: > if (should_skip($entry)) { > continue 2; > } > > $db->insert($entry); > } > } > > $processed++; > } > > I'm also noticing that the RFC still does not explain *why* the decision > for `break` to target `using()` has been made. For your reference, my > last email asking that question is this one: > https://news-web.php.net/php.internals/129771. I didn't receive a reply > to that email either (and neither do the list archives have a reply).
I'm pretty sure I did explain in this thread somewhere... In short, we want a way to be able to terminate the using block early in a success case. An error case is easy (throw), but for a success case we cannot use return, as that will return from the function. Technically "goto and your own label" would work, but I really hope we don't need to get into a discussion about why making goto the only way to solve something is a bad idea... break is the natural keyword for that, as it already means "stop this control structure and go to the end of it." continue means "stop this iteration of a control structure and go to the next one." But in this case, there is no next one. switch makes it an alias for break, for whatever reason lost to history, but given that it now throws a warning that seems to now be considered a mistake, so we don't see a reason to propagate that mistake.
>> 2. Several people (including us) were uncomfortable with using a >> boolean return from the exitContext() method. While that is what >> Python does, it is indeed not self-evident how it works. (Should true >> mean "true, I'm done" or "true, rethrow"?) We debated using an enum >> value, but that appeared to be too verbose. >> >> Instead, we decided that exitContext() should return ?Throwable, which >> is the same thing it is passed. In a success case, it is passed null. >> In a failure case, it is passed a throwable. So it can then return >> null (meaning "we're done, nothing else to do here") or a throwable, >> which will then get thrown. Since in most cases an error should be >> allowed to propagate, it means simply calling `return $exception` at >> the end of the method will "do the right thing" 95% of the time. >> Simple and easy and self-documenting. (If there's a reason to wrap and >> rethrow the exception, do that and return the new exception. Or to >> swallow the exception and not propagate it, return null.) > > That sounds like a “throw” statement with extra steps [1]. While nothing > stopped you from writing `throw new SomeException();` within > `exitContext()` with the `bool` return value, it at least *encouraged* > you to not replace the original Exception with another Exception > entirely and to just make a decision between “suppress” or “not > suppress”. Now `return` is completely equivalent to `throw` (at least as > long as `exitContext()` doesn't contain a `catch()` itself) adding even > more layers of “using() is magically including behavior of other > language constructs” that will be hard to reason about for humans and > machines alike.
If you have an alternate suggestion for how to achieve this functionality, now is the time to propose it. Behaviors in the order they're likely to happen (I'd expect): - Success case, there is no exception - Keep propagating the exception. - The exception stops here. - We're catching the exception and wrapping it in another exception with more useful data on it (exceptions can do that), and then throwing that. The setup we have now solves all four cases with fairly self-evident code, and handles the first two cases in the exact same code so most people won't need to really think about it. If you have a better suggestion, please do share. --Larry Garfield

Rowan Tommins [IMSoP]

154 days ago
On 30 March 2026 19:48:05 BST, Larry Garfield <larry@garfieldtech.com> wrote:
> ... break is the natural keyword for that, as it already means "stop this control structure and go to the end of it." > >continue means "stop this iteration of a control structure and go to the next one." But in this case, there is no next one. switch makes it an alias for break, for whatever reason lost to history, but given that it now throws a warning that seems to now be considered a mistake, so we don't see a reason to propagate that mistake.
I agree with Tim that making break and continue define targets differently is a really bad idea. It's fine for a single "break;" or "continue;", but with PHP's count-based targeting, it would lead to cases like this: if ( definitely_right($loop_item) ) { $found_item = $loop_item; break 4; } elseif ( definitely_wrong($loop_item) ) { // targeting the same loop, but there's a couple of "using" or "switch" blocks in between continue 2; } If "break 2" terminates a "using" block, there are only two sane behaviours for "continue 2": - terminate that same block, as though it was a single-iteration loop - throw an Error, because the operation is not meaningful Looking back at the discussion of "continue targeting switch", I see I made the same point back then: https://externals.io/message/102393#102500 The consensus in that discussion was to *only* add a Warning, with no plan for further changes. It's not a deprecation, or a workaround for hard to change legacy behaviour; it's just a helpful hint to the developer that their code might have a mistake in it. Regards, Rowan Tommins [IMSoP]

Tim Düsterhus

152 days ago
Hi Am 2026-03-30 20:48, schrieb Larry Garfield:
>> “Sleaker” is not a word my dictionary understands. Was this a typo for >> “sleeker” in the sense of “refined”? > > Yes, typo. Sleeker in the sense of "fewer bumpy parts on it."
Okay. I think that updated semantics failed in that goal and instead added additional bumpy parts.
>>> 1. We went back and forth on the `continue` question several times, >>> before coming to the conclusion that `continue` is a tool for looping >>> structures only. That `switch` also uses it is just `switch` being >>> silly because reasons, and there is no reason `using` must inherit >>> its >>> weirdness. Therefore, `continue` inside a `using` block now means >>> nothing at all. `continue` will ignore it, the same way it ignores >>> an >>> `if` statement. >> >> I fail to see how “making break; and continue; behave inconsistently” >> is >> making the RFC (and by extension) the language any more consistent. In >> this following example snippet it's not at all obvious that the >> `break` >> is behaving incorrectly by targeting the `using()` with `continue` >> targeting the `foreach()`, despite both using the same “number”: >> >> $processed = 0; >> foreach ($entries as $entry) { >> using ($db->transaction()) { >> switch ($entry['type']) { >> case 'EOF': >> break 2; >> default: >> if (should_skip($entry)) { >> continue 2; >> } >> >> $db->insert($entry); >> } >> } >> >> $processed++; >> } >> >> I'm also noticing that the RFC still does not explain *why* the >> decision >> for `break` to target `using()` has been made. For your reference, my >> last email asking that question is this one: >> https://news-web.php.net/php.internals/129771. I didn't receive a >> reply >> to that email either (and neither do the list archives have a reply). > > I'm pretty sure I did explain in this thread somewhere... In short, we > want a way to be able to terminate the using block early in a success > case. An error case is easy (throw), but for a success case we cannot > use return, as that will return from the function.
That is a self-referential argument - you made the decision, because you wanted to make the decision. I was looking for an explanation why context managers are sufficiently special that they *need* support for exiting their associated block early *and* why existing control structures are insufficient to handle that. `try` for example does not support exiting the block early and given that the primary semantics of context managers are that of a try-catch-finally, it is reasonable to ask what makes context managers different. I also note that none of the existing examples in the RFC make use of `break`, this capability only gets a passing mention. Please provide a use case.
> Technically "goto and your own label" would work, but I really hope we > don't need to get into a discussion about why making goto the only way > to solve something is a bad idea...
No, please elaborate.
> break is the natural keyword for that, as it already means "stop this > control structure and go to the end of it."
Quoting my the previous email that I linked in the email you were replying to:
> “break does a different thing here than it does in if, else, try, > catch, finally, namespace, and declare” doesn't seem like a > straightforward approach to me.
-
> continue means "stop this iteration of a control structure and go to > the next one." But in this case, there is no next one. switch makes > it an alias for break, for whatever reason lost to history, but given > that it now throws a warning that seems to now be considered a mistake, > so we don't see a reason to propagate that mistake.
I think Rowan explained that well in his reply.
> If you have an alternate suggestion for how to achieve this > functionality, now is the time to propose it.
As implied by my email, I believe that throwing exceptions should be the job of the `throw` keyword, not the job of the `return` keyword. Otherwise users are going to wonder what makes `return new SomeException();` different from `throw new SomeException()` in that case. Best regards Tim Düsterhus

Larry Garfield

147 days ago
On Thu, Apr 2, 2026, at 5:13 AM, Tim Düsterhus wrote:
>> continue means "stop this iteration of a control structure and go to >> the next one." But in this case, there is no next one. switch makes >> it an alias for break, for whatever reason lost to history, but given >> that it now throws a warning that seems to now be considered a mistake, >> so we don't see a reason to propagate that mistake. > > I think Rowan explained that well in his reply. > >> If you have an alternate suggestion for how to achieve this >> functionality, now is the time to propose it. > > As implied by my email, I believe that throwing exceptions should be the > job of the `throw` keyword, not the job of the `return` keyword. > Otherwise users are going to wonder what makes `return new > SomeException();` different from `throw new SomeException()` in that > case. > > Best regards > Tim Düsterhus
We've updated the RFC to address the break question (new section), the continue question (which is now a secondary vote), and expanded the reasoning for the `return $e` decision, including reference to the original Python PEP which explains the need for a distinction. --Larry Garfield

Rowan Tommins [IMSoP]

146 days ago
On 07/04/2026 17:20, Larry Garfield wrote:
> We've updated the RFC to address the break question (new section), the continue question (which is now a secondary vote)
You are still relying on an incorrect explanation of the relationship between "switch" and "continue":
> This behavior is due to a quirk of PHP's design, where |switch| is
treated as a looping structure, which most languages do not. "continue" is counted for switch statements not because it is "treated as a loop", but because PHP has numbered break and continue targets. Numbering break targets differently from continue targets would be extremely confusing, so they have to target the same list of constructs. There was strong consensus on this point in the previous discussion. I can only see three defensible options: 1) Support neither "break" nor "continue". This would be consistent with "if", "try", etc, which you have used as comparisons in the RFC. 2) Support "break", and have a Warning on "continue". This would be consistent with "switch", and harmless. 3) Support "break", and have an Error on "continue". This would be novel behaviour, but not dangerous. Personally, I'm leaning towards option 1 - the case for "break" feels weak to me. Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

146 days ago
On Tue, Apr 7, 2026, at 5:27 PM, Rowan Tommins [IMSoP] wrote:
> On 07/04/2026 17:20, Larry Garfield wrote: >> We've updated the RFC to address the break question (new section), the continue question (which is now a secondary vote) > > > You are still relying on an incorrect explanation of the relationship > between "switch" and "continue": > >> This behavior is due to a quirk of PHP's design, where `switch` is treated as a looping structure, which most languages do not. > > > > "continue" is counted for switch statements not because it is "treated > as a loop", but because PHP has numbered break and continue targets. > Numbering break targets differently from continue targets would be > extremely confusing, so they have to target the same list of constructs. > > There was strong consensus on this point in the previous discussion.
Quoting from the Nikita post that is linked from the RFC: --- a) In PHP "switch" is considered a looping structure, for this reason "break" and "continue" both apply to "switch", as aliases. For PHP, these are reasonable semantics, as PHP supports multi-level breaks. It would be very questionable if "break N" and "continue N" could refer to different loop structures just because there is a "switch" involved somewhere. --- Switch being considered a looping structure does qualify as a "quirk" in my book. If we want break/continue to still always have the same numbering, then "do the same as switch, even though it is a Warning" is the only option. Not having an early-success syntax at all is not an option on the table. --Larry Garfield

Rowan Tommins [IMSoP]

146 days ago
On 8 April 2026 17:07:38 BST, Larry Garfield <larry@garfieldtech.com> wrote:
>Switch being considered a looping structure does qualify as a "quirk" in my book.
I had a look at really old code on https://museum.php.net and from what I can make out, PHP/FI 2.0 had single-level "break" only as part of the "switch" syntax; there was no way to terminate loops early. PHP 3.0 added a general-purpose "break" and "continue", with both keywords taking an optional argument - in fact, both were implemented by the same C function. The existing use of "break" for switch statements just became part of this more general feature. I guess you could argue that that means "treating switch as a looping construct", but I honestly can't think what the alternative would have been, other than using a different keyword. If PHP has a "quirk" it is that "break" and "continue" both take a numeric argument. Any new use of either keyword would need to deal with that quirk, even if "switch" was removed from the language completely. Rowan Tommins [IMSoP]

Tim Düsterhus

140 days ago
Hi Am 2026-04-08 00:27, schrieb Rowan Tommins [IMSoP]:
> Personally, I'm leaning towards option 1 - the case for "break" feels > weak to me.
I agree here. For the example use case that is provided in the RFC, I feel that the `break;` is making intent much less clear. When dropping the comment that makes sense in the context of an RFC, but not inside a program, we have. using(file_open('records.csv', 'rw') => $fp) { while ($line = fgetcsv($fp) { if ($line[0] === $record['id']) { break 2; } } fputcsv($fp, $record); } And here I'm wondering what the “high-level” purpose of that `break;` is. I would either need to add a comment: using(file_open('records.csv', 'rw') => $fp) { while ($line = fgetcsv($fp) { if ($line[0] === $record['id']) { // If the record already exists, we are done with processing the file. break 2; } } fputcsv($fp, $record); } or I can just use a self-explanatory variable: using(file_open('records.csv', 'rw') => $fp) { $found = false; while ($line = fgetcsv($fp) { if ($line[0] === $record['id']) { $found = true; break; } } if (!$found) { fputcsv($fp, $record); } } Using a variable is also more easily extendible to a “if found then X else Y” situation. A goto would also be clearer in intent, because I can give it a name and thus it effectively serves the purpose of a simplified comment. It's also easy to find where the control flow jumps by scanning for the label, without need to manually count control structures (which is particularly complicated when not all of them use braces). In fact jumping out of multiple control structures is explicitly documented as one use case on the `goto` documentation at https://www.php.net/manual/en/control-structures.goto.php:
> […] a common use is to use a goto in place of a multi-level break.
Insofar I don't see how the RFC's claim of “goto is generally discouraged, as it is less structured than break or continue” is backed up by evidence. using(file_open('records.csv', 'rw') => $fp) { while ($line = fgetcsv($fp) { if ($line[0] === $record['id']) { goto record_found; } } fputcsv($fp, $record); } record_found: Best regards Tim Düsterhus

Tim Düsterhus

140 days ago
Larry, On 4/7/26 18:20, Larry Garfield wrote:
> the continue question (which is now a secondary vote)
I'm really struggling with finding appropriate words in my reply here. It is not at all clear to me how both Rowan's and my replies could lead to a conclusion of "neither approach is obviously better and both have downsides" and the addition of a secondary vote, particularly one with voting options using a "suggestive question" wording. Secondary votes are for multiple equally-valid options that *directly* relate to the RFC in question, not to have a backdoor to ship changes to PHP with less than a 2/3 majority. The RFC policy is quite clear on that:
> Combining multiple proposals into one RFC MUST NOT be used to turn a > primary vote into a secondary vote.
In this case this secondary vote would (potentially; depending on the result) break the expectation that exchanging `break` with `continue` and vice versa will continue (pun not intended) to target the same control structure and add more special cases to the languages. This is a semantic change that is unrelated to context managers, and thus would be something requiring a 2/3 vote - or better: Something that should just not happen. I find it extremely disrepectful to use loaded language like "quirk" to refer previously established design decision that you don't understand the reasons for or that you disagree with. With regard to the voting options:
> Ignore using blocks entirely, like try
is drawing a faulty comparison, because `break` also ignores `try`. This is not special behavior of `continue`. There is a reason as to why things are the way they are. Please do your research instead of making assumptions and leaving it to others to point out how the RFC would break PHP's existing well-established language design. Best regards Tim Düsterhus

Larry Garfield

140 days ago
On Tue, Apr 14, 2026, at 8:35 AM, Tim Düsterhus wrote:
> Larry, > > On 4/7/26 18:20, Larry Garfield wrote: >> the continue question (which is now a secondary vote) > > I'm really struggling with finding appropriate words in my reply here. > > It is not at all clear to me how both Rowan's and my replies could lead > to a conclusion of "neither approach is obviously better and both have > downsides" and the addition of a secondary vote, particularly one with > voting options using a "suggestive question" wording. > > Secondary votes are for multiple equally-valid options that *directly* > relate to the RFC in question, not to have a backdoor to ship changes to > PHP with less than a 2/3 majority. The RFC policy is quite clear on > that: > >> Combining multiple proposals into one RFC MUST NOT be used to turn a >> primary vote into a secondary vote. > > In this case this secondary vote would (potentially; depending on the > result) break the expectation that exchanging `break` with `continue` > and vice versa will continue (pun not intended) to target the same > control structure and add more special cases to the languages. This is a > semantic change that is unrelated to context managers, and thus would be > something requiring a 2/3 vote - or better: Something that should just > not happen. > > I find it extremely disrepectful to use loaded language like "quirk" to > refer previously established design decision that you don't understand > the reasons for or that you disagree with.
I similarly find it extremely disrespectful to presume malicious intent where there is none. The "loaded language" you refer to is in reference to a statement by Nikita Popov in the thread that Rowan previously linked to. Quoting him again:
> In PHP "switch" is considered a looping structure, for this reason
"break" and "continue" both apply to "switch", as aliases. That would certainly qualify as a "quirk" in my book, because that's kinda weird and unlike any other language. Now, if that description is not accurate (or was at the time and no longer is), that's a different question. I have indeed not checked that part of the engine; if you would like to provide data to show Nikita's description was/is wrong, I will happily accept it. As far as it being a "backdoor" change to another feature... once again, you seem to be assuming subterfuge or malicious intent without evidence. The secondary vote was, specifically, "here's a new feature, `using`, how should this existing feature, `continue`, interact with it?" That is directly related to the RFC in question; it would be a meaningless question to ask outside of an RFC introducing `using`. You have made your opinion on this feature quite clear. Others disagree. Accusing us of trying to sneak around the rules because we don't agree with your argument is completely uncalled for. If that part of the RFC is enough for you to vote against the RFC over, that is certainly your right, and I've certainly voted against RFCs I otherwise supported myself due to smaller design decisions. But that is not grounds to make the baseless accusations you're making here. Prior to your post, Arnaud and I had already discussed it further and decided to revert back to the original RFC design, with `continue` mirroring `switch`'s behavior (do the same as `break` but trigger a Warning), based on Rowan's feedback. I dislike the idea of a new feature that includes a Warning out of the gate (which is why it's been an open question of discussion), but it seems to be the least-bad option. I'll be updating the RFC accordingly shortly. (This will eliminate the secondary vote.) --Larry Garfield

Tim Düsterhus

139 days ago
Hi Am 2026-04-14 20:18, schrieb Larry Garfield:
> I similarly find it extremely disrespectful to presume malicious intent > where there is none.
To provide context as to what happened from my point of view: Both Rowan and I explained why "continue == break" is important for language consistency, with Rowan even doing the research in the list archives to provide the explanation that your email (https://news-web.php.net/php.internals/130500) claimed was "lost to history". Neither of us received any further reply on-list, until the generic "we updated the RFC" roughly a week after. Looking at the changes in the RFC then reveals that the "historic" explanation had been taken out of context (see below) and a secondary vote with misleading voting options had been added. The only possible explanations I could find for that outcome were "lack of diligence", "misunderstanding", and "intent". Given this RFC has two authors, I assumed that any changes to the RFC would have been cross-checked by both authors, which I would expect to catch accidental mistakes arising from a lack of diligence or a misunderstanding (e.g. due to language barriers), which then left "intent". If this assumption of "intent" is incorrect and it's one of the other options instead, I apologize for drawing this incorrect conclusion. But I find it problematic that this kind of oversight makes it into 2-author RFC.
> The "loaded language" you refer to is in reference to a statement by > Nikita Popov in the thread that Rowan previously linked to. Quoting > him again: > >> In PHP "switch" is considered a looping structure, for this reason > "break" and "continue" both apply to "switch", as aliases.
That is correct, but it is taken out of context. The rest of the paragraph that immediately follows that quote is explaining why "switch is considered a looping structure". Let me include it here:
> [...] For PHP, these are reasonable semantics, as PHP supports > multi-level breaks. It would be very questionable if "break N" and > "continue N" could refer to different loop structures just because > there is a "switch" involved somewhere.
And other emails in that discussion thread, including Rowan's own (which he linked in his previous reply), agreed with that. Given that the discussion back then resulted in a change to the language (even if not via a formal accepted RFC), I believe it is a reasonable assertion that "break == continue" is a intentional part of PHP's language design that cannot be disregarded in passing.
> That would certainly qualify as a "quirk" in my book, because that's > kinda weird and unlike any other language. Now, if that description is > not accurate (or was at the time and no longer is), that's a different > question. I have indeed not checked that part of the engine; if you > would like to provide data to show Nikita's description was/is wrong, I > will happily accept it.
I refer to Rowan's email here: https://news-web.php.net/php.internals/130591. In any case "quirk" or similar (negatively) connotated words are inappropriate for use in an RFC, which is a document that should accurately and unambiguously describe a change that is proposed to be made to PHP (or the PHP project's governance in case of policy RFCs). Readers should be able to make their own judgement based on the stated facts and well-justified decisions. A little more "flowery" language to describe the high-level goals of the RFC can be okay - particularly in the "Introduction" and "Examples" sections, which are intended to showcase what the RFC will enable. But deep in the semantics section of an RFC precise language is important for readers to build an informed decision.
> The secondary vote was, specifically, "here's a new feature, `using`, > how should this existing feature, `continue`, interact with it?" That > is directly related to the RFC in question; it would be a meaningless > question to ask outside of an RFC introducing `using`.
I don't think it is reasonable to consider "continue" and "break" to be two separate features given how closely they are related, both in PHP and in other programming language. Both in PHP's implementation and in PHP's documentation. Tutorials introducing "break" are usually introducing "continue" at the same time. And following from that, the secondary vote would affect the existing design of an unrelated feature.
> You have made your opinion on this feature quite clear.
I believe that sufficient references have been provided that allow me to reasonably say that this is not just an "opinion". For the related "should break target using() in the first place" question this is something one can have an "opinion" on - one that I personally disagree with. The difference is that neither choice there breaks long-standing user expectations with regard to the language's behavior, which makes them equally valid.
> This will eliminate the secondary vote.
Thank you. Best regards Tim Düsterhus

Alex Pierstoval Rock

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

Larry Garfield

139 days ago
On Tue, Apr 14, 2026, at 3:42 PM, Alex Rock wrote:
> Le 29/03/2026 à 13:14, Tim Düsterhus a écrit : >> // ... > > > I'm sorry that I'm arriving super late in the process, but I've > subscribed to the mailing list only recently. > > To give here my 2cents: > > I think this RFC addresses something good (scoped code in the middle of > a bigger chunk of code), but does it maybe a bit too bloated-ly.
Hi Alex. I think you're missing the point of this construct. What you're describing is much closer to the recently-declined `let` scoping keyword (https://wiki.php.net/rfc/optin_block_scoping). The entire point of context managers is to abstract away and make reusable the try-catch-finally logic. That logic needs to live somewhere so that it can be reused. That is what the ContextManager object is for. It's not a complication; it's the entire purpose of the RFC. Since you just recently joined the list you would have missed the earlier discussion in this thread of closures. (See https://externals.io/message/129077). In short, closures are what people use today for this use case, but they're a PITA because they don't support auto-capture (unless you're using the single-line version). That makes them less common, because they're a pain to use in this case. In addition, they do create a new scope, which... is not what we want in this case. Not creating a new scope is a feature, not a bug. Also, this email serves as notice that I've updated the RFC again to make continue an alias of break, as it was originally. --Larry Garfield

Alex Pierstoval Rock

139 days ago
Le 15/04/2026 à 17:10, Larry Garfield a écrit :
> I think you're missing the point of this construct. What you're describing is much closer to the recently-declined `let` scoping keyword (https://wiki.php.net/rfc/optin_block_scoping).
Yep, I would have been much more okay with a revised version of the `let` scoping keyword, though the implementation also seems quite complex too.
> The entire point of context managers is to abstract away and make reusable the try-catch-finally logic. That logic needs to live somewhere so that it can be reused. That is what the ContextManager object is for. It's not a complication; it's the entire purpose of the RFC. > > Since you just recently joined the list you would have missed the earlier discussion in this thread of closures. (Seehttps://externals.io/message/129077).
I have read a few of the messages on externals.io, maybe not everything, but your answer indeed makes things a bit clearer. It mostly makes me think that this RFC is a bit complex and I don't see /many/ userland use-cases that would actually need this. IMO, it tries to /make some safeguards/ implicit, like closing resources. I recognize that it's useful for this case, but does PHP really need a new feature this big "just" to close resources 10 milliseconds earlier in a process that takes 100ms anyway? The benefits would only go to projects with huge amount of concurrent calls, which are not all of them.
> In short, closures are what people use today for this use case, but they're a PITA because they don't support auto-capture (unless you're using the single-line version). That makes them less common, because they're a pain to use in this case. In addition, they do create a new scope, which... is not what we want in this case. Not creating a new scope is a feature, not a bug.
No auto-capture in closures is IMO an actual *good* thing, because it avoids having issues with the current world of JS/TS where auto-capture is default, and it brings problems with references. My suggestion with the `scoped` keyword also carries auto-capture by design, which makes the "capturing system" (shadowing, references, etc.) less intuitive in the first place. It's "yet another thing to keep in mind" at first, but for legacy apps renovators like me, it's mostly "they haven't kept this in mind" debugging hell ^^', but considering there's always worse, it's negligible, especially if it's intended to be a new feature. Still: creating a new scope makes things safer. It creates a new stack with its own pointers, and frees it at finish. They are a pain to use for now mostly because there's more code to write, it needs to be called (or auto-called with `()` after it), and capture must be explicit via `use` . The `scoped` keyword proposal automates all this with existing PHP features, while the ContextManager adds new features for that, and we don't know (yet) the overhead. Sorry for being this picky :)

Larry Garfield

139 days ago
On Wed, Apr 15, 2026, at 11:39 AM, Alex Rock wrote:
> I have read a few of the messages on externals.io, maybe not > everything, but your answer indeed makes things a bit clearer. It > mostly makes me think that this RFC is a bit complex and I don't see > *many* userland use-cases that would actually need this.
The RFC lists several. It's also all over Python code and has been for years. Though to be sure, people writing `using` blocks will outnumber those writing custom context managers 100:1, I imagine. That's fine.
> IMO, it tries to *make some safeguards* implicit, like closing > resources. I recognize that it's useful for this case, but does PHP > really need a new feature this big "just" to close resources 10 > milliseconds earlier in a process that takes 100ms anyway? The benefits > would only go to projects with huge amount of concurrent calls, which > are not all of them.
"Big" seems like a fairly subjective word to use here. It's just an interface and some desugaring. Compared to many of the changes already added just in this release, this is a small impact RFC. :-) And we know it's overhead: It's a couple of instructions that you would have written manually anyway, plus 2 method calls, which you may have had anyway. So, negligible in practice.
> No auto-capture in closures is IMO an actual *good* thing, because it > avoids having issues with the current world of JS/TS where auto-capture > is default, and it brings problems with references.
Almost every language has closures with capture now. Only two require explicitly listing values to capture: PHP and C++. Everyone else figured out how to do it safely. The problem with JS is that it's by ref capture. But that's off topic, as we're discussing a feature that doesn't impact closures at all. If you want to make a separate proposal that leverages closures, you're welcome to do so, but that's not what this RFC is about. --Larry Garfield

Rob Landers

140 days ago
On Tue, Nov 4, 2025, at 21:13, 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. :-) > > -- > Larry Garfield > larry@garfieldtech.com >
Hi Larry/Arnaud, This is a pretty exciting thread and fascinating proposal. That being said, I have a couple of subtle questions that don't seem to be answered in the (very long) thread or the RFC itself -- If I missed it, please let me know: 1. What happens if a Fiber is suspended in the using block and never resumed? When is the using block released to clean up the context? 2. There's still no mention of how this should affect debugging, will we see the "desugared" or "sugared" version? Is that even a concern for the RFC? 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing? 4. Looking at the desugared form ... I'm a bit confused: if exitContext is called during the finally path and returns an exception, it is just swallowed? But if it is thrown, it won't be? 5. That being said, I don't think the RFC shares with us when we should return an exception vs. throw an exception. — Rob

Rob Landers

140 days ago
On Tue, Apr 14, 2026, at 16:18, Rob Landers wrote:
> > > On Tue, Nov 4, 2025, at 21:13, 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. :-) >> >> -- >> Larry Garfield >> larry@garfieldtech.com >> > > Hi Larry/Arnaud, > > This is a pretty exciting thread and fascinating proposal. That being said, I have a couple of subtle questions that don't seem to be answered in the (very long) thread or the RFC itself -- If I missed it, please let me know: > 1. What happens if a Fiber is suspended in the using block and never resumed? When is the using block released to clean up the context? > 2. There's still no mention of how this should affect debugging, will we see the "desugared" or "sugared" version? Is that even a concern for the RFC? > 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing? > 4. Looking at the desugared form ... I'm a bit confused: if exitContext is called during the finally path and returns an exception, it is just swallowed? But if it is thrown, it won't be? > 5. That being said, I don't think the RFC shares with us when we should return an exception vs. throw an exception. > > — Rob
Maybe the desugared version should look more like this? } catch (\Throwable $e) { try { $__mgr->exitContext($e); } catch (\Throwable $cleanupException) { throw new ContextManagerException( $cleanupException->getMessage(), previous: $e ); } throw $e; } — Rob

Larry Garfield

139 days ago
On Tue, Apr 14, 2026, at 9:27 AM, Rob Landers wrote:
> On Tue, Apr 14, 2026, at 16:18, Rob Landers wrote: >> >> >> On Tue, Nov 4, 2025, at 21:13, 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. :-) >>> >>> -- >>> Larry Garfield >>> larry@garfieldtech.com >>> >> >> Hi Larry/Arnaud, >> >> This is a pretty exciting thread and fascinating proposal. That being said, I have a couple of subtle questions that don't seem to be answered in the (very long) thread or the RFC itself -- If I missed it, please let me know:
>> 1. What happens if a Fiber is suspended in the using block and never resumed? When is the using block released to clean up the context?
Since it decomposes to a try-catch-finally, it will exit whenever the finally block would have run if you'd just typed out try-catch-finally yourself. Arnaud checked, and confirmed that when the fiber is destroyed the using block will exit in a success case (ie, exitContext(null)).
>> 2. There's still no mention of how this should affect debugging, will we see the "desugared" or "sugared" version? Is that even a concern for the RFC?
Error messages would see the original code, so "error on line X" would be based on the original `using` block. That's the same as any other desugaring we already do. (PIpes, PFA, constructor promotion, etc.) Debuggers will see the materialized opcodes, again, the same other desugaring cases.
>> 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing?
There's a subtle but important difference here: An exception passed through exitContext() is the original exception from lower in the call stack, and its backtrace will be the original location of the error. An exception thrown from within exitContext() itself indicates a failure that the Context Manager is responsible for, usually an error in the exitContext() logic itself. Technically a Context Manager can wrap-and-rethrow the exception if it wants, but then it is "claiming ownership" over it, just like in any other case of wrap-and-rethrow. Our expectation is that 90% of the time, "let the exception propagate up unimpeded" is the desired behavior. This approach makes "return $e" the right thing to do almost-always, which is nice and simple to remember. See the "return values and exception handling" section for a discussion of this in more detail. As I said in a previous reply, our constraints are different than Python's so we end up with a different solution. If you have a suggestion for an alternate approach to the problem, we're happy to listen.
>> 4. Looking at the desugared form ... I'm a bit confused: if exitContext is called during the finally path and returns an exception, it is just swallowed? But if it is thrown, it won't be?
The finally path is only reached in case of a successful exit. Therefore there is no exception to pass in, and thus returning an exception is meaningless. If exitContext() throws a new exception of its own (which would indicate an error in its own logic), that will just bubble up past the `using` block entirely, which is what we want.
>> 5. That being said, I don't think the RFC shares with us when we should return an exception vs. throw an exception.
See the "return values and exception handling" section. If something there isn't clear, let me know and I will try to clarify further.
>> — Rob > > Maybe the desugared version should look more like this? > > } catch (\Throwable $e) { > try { > $__mgr->exitContext($e); > } catch (\Throwable $cleanupException) { > throw new ContextManagerException( > $cleanupException->getMessage(), > previous: $e > ); > } > throw $e; > } >
I'm not sure I see a reason to force any new exceptions to be only of the ContextManagerException type. If there's a TypeError inside exitContext() or something, I'd expect that to be propagated as a TypeError. --Larry Garfield

Rob Landers

139 days ago
On Wed, Apr 15, 2026, at 16:52, Larry Garfield wrote:
> On Tue, Apr 14, 2026, at 9:27 AM, Rob Landers wrote: > > On Tue, Apr 14, 2026, at 16:18, Rob Landers wrote: > >> > >> > >> On Tue, Nov 4, 2025, at 21:13, 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. :-) > >>> > >>> -- > >>> Larry Garfield > >>> larry@garfieldtech.com > >>> > >> > >> Hi Larry/Arnaud, > >> > >> This is a pretty exciting thread and fascinating proposal. That being said, I have a couple of subtle questions that don't seem to be answered in the (very long) thread or the RFC itself -- If I missed it, please let me know: > > >> 1. What happens if a Fiber is suspended in the using block and never resumed? When is the using block released to clean up the context? > > Since it decomposes to a try-catch-finally, it will exit whenever the finally block would have run if you'd just typed out try-catch-finally yourself. Arnaud checked, and confirmed that when the fiber is destroyed the using block will exit in a success case (ie, exitContext(null)). > > >> 2. There's still no mention of how this should affect debugging, will we see the "desugared" or "sugared" version? Is that even a concern for the RFC? > > Error messages would see the original code, so "error on line X" would be based on the original `using` block. That's the same as any other desugaring we already do. (PIpes, PFA, constructor promotion, etc.) Debuggers will see the materialized opcodes, again, the same other desugaring cases. > > >> 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing? > > There's a subtle but important difference here: An exception passed through exitContext() is the original exception from lower in the call stack, and its backtrace will be the original location of the error. An exception thrown from within exitContext() itself indicates a failure that the Context Manager is responsible for, usually an error in the exitContext() logic itself. > > Technically a Context Manager can wrap-and-rethrow the exception if it wants, but then it is "claiming ownership" over it, just like in any other case of wrap-and-rethrow. > > Our expectation is that 90% of the time, "let the exception propagate up unimpeded" is the desired behavior. This approach makes "return $e" the right thing to do almost-always, which is nice and simple to remember. > > See the "return values and exception handling" section for a discussion of this in more detail. As I said in a previous reply, our constraints are different than Python's so we end up with a different solution. If you have a suggestion for an alternate approach to the problem, we're happy to listen. > > >> 4. Looking at the desugared form ... I'm a bit confused: if exitContext is called during the finally path and returns an exception, it is just swallowed? But if it is thrown, it won't be? > > The finally path is only reached in case of a successful exit. Therefore there is no exception to pass in, and thus returning an exception is meaningless. If exitContext() throws a new exception of its own (which would indicate an error in its own logic), that will just bubble up past the `using` block entirely, which is what we want. > > >> 5. That being said, I don't think the RFC shares with us when we should return an exception vs. throw an exception. > > See the "return values and exception handling" section. If something there isn't clear, let me know and I will try to clarify further. > > >> — Rob > > > > Maybe the desugared version should look more like this? > > > > } catch (\Throwable $e) { > > try { > > $__mgr->exitContext($e); > > } catch (\Throwable $cleanupException) { > > throw new ContextManagerException( > > $cleanupException->getMessage(), > > previous: $e > > ); > > } > > throw $e; > > } > > > > I'm not sure I see a reason to force any new exceptions to be only of the ContextManagerException type. If there's a TypeError inside exitContext() or something, I'd expect that to be propagated as a TypeError. > > --Larry Garfield
Thanks Larry, this clears things up for me. The example I had in mind is distinguishing between errors in a transaction: class DatabaseTransaction implements ContextManager { public function __construct(private PDO $connection) {} public function enterContext(): PDO { $this->connection->beginTransaction(); return $this->connection; } public function exitContext(?\Throwable $e = null): ?\Throwable { if ($e) { $this->connection->rollback(); // PDO throws: server has gone away } else { $this->connection->commit(); } return $e; } } // Application code: using ($db->transaction() => $conn) { $conn->execute('INSERT INTO orders ...'); // throws ValidationException } The application will see the PDOException about the server going away, but the original ValidationException that caused the rollback attempt is lost entirely. My point with my suggestion wasn't to hide an exception behind a specific type, but to use exception chaining native to PHP to preserve both independent failures in a way that an application can understand what actually happened. In other words, as a developer, all I'd see is a rollback failed due to a server disconnection. I'd be missing what caused the rollback in the first place. I'd be fine if the desugared catch path simply attached the original exception as `$previous` on whatever escapes `exitContext()`, so the root cause isn't lost. That's a one-line change in the engine: just set the previous property at the bottom of the cleanup exception's chain before rethrowing. — Rob

Tim Düsterhus

133 days ago
Hi Am 2026-04-15 16:52, schrieb Larry Garfield:
> The finally path is only reached in case of a successful exit. > Therefore there is no exception to pass in, and thus returning an > exception is meaningless. […]
That's not at all how I understood the RFC text (and I admittedly didn't look at the desugaring):
> If exitContext() returns a throwable (either a new one or the one it > was passed), it will be rethrown.
I understood the “it” in “it will be rethrown” as “the returned Throwable is thrown”, not “the Throwable that was caught is thrown”. That is also what is mentioned in your email https://news-web.php.net/php.internals/130413:
> or a throwable, which will then get thrown
and
> If there's a reason to wrap and rethrow the exception, do that and > return the new exception
And that is what the second half of my email https://news-web.php.net/php.internals/130479 is based on, particularly the joke in the footnote. Why has this misunderstanding not been pointed out back then? ------------ So based on the desugaring that Rob thankfully looked at, of all the information in a `?\Throwable` return type, only a single bit is used - and only in some cases. I don't see how we can meaningfully explain to users that throwing away all the other information is the expected behavior. Best regards Tim Düsterhus

Rowan Tommins [IMSoP]

133 days ago
On 15 April 2026 15:52:17 BST, Larry Garfield <larry@garfieldtech.com> wrote:
>>> 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing? > >There's a subtle but important difference here: An exception passed through exitContext() is the original exception from lower in the call stack, and its backtrace will be the original location of the error. An exception thrown from within exitContext() itself indicates a failure that the Context Manager is responsible for, usually an error in the exitContext() logic itself.
PHP collects traces when exceptions are constructed, not when they're thrown, so this is a distinction without a difference. From the outside, it's impossible to tell the difference between "return $e;" and "throw $e;". That means you have the following options: - throw the passed exception unchanged - return the passed exception, which is equivalent to throwing it - throw a new exception, or fail to catch an exception in the cleanup logic, which as Rob points out will hide the passed exception unless you remember to attach it as $previous - return a new exception, which according to the current RFC text will be completely ignored (is "throw $e" supposed to say "throw $__ret"?) It does seem like it would be more straightforward to have the return value be "void", and leave it to the implementation to throw or not. In practice, as you say, "throw unchanged" will be common, but unless that's the behaviour of a *null* return (i.e. the default if not opted out), "throw $e;" seems the natural boilerplate for that. Regards, Rowan Tommins [IMSoP]

Larry Garfield

132 days ago
On Tue, Apr 21, 2026, at 7:16 AM, Rowan Tommins [IMSoP] wrote:
> On 15 April 2026 15:52:17 BST, Larry Garfield <larry@garfieldtech.com> wrote: >>>> 3. I will say it is weird to have exitContext return an exception; but what happens if an exception is thrown during exitContext? Why not just have it return void and throw if you need to throw instead of having two paths to the same thing? >> >>There's a subtle but important difference here: An exception passed through exitContext() is the original exception from lower in the call stack, and its backtrace will be the original location of the error. An exception thrown from within exitContext() itself indicates a failure that the Context Manager is responsible for, usually an error in the exitContext() logic itself. > > > PHP collects traces when exceptions are constructed, not when they're > thrown, so this is a distinction without a difference. From the > outside, it's impossible to tell the difference between "return $e;" > and "throw $e;". > > That means you have the following options: > > - throw the passed exception unchanged > - return the passed exception, which is equivalent to throwing it > - throw a new exception, or fail to catch an exception in the cleanup > logic, which as Rob points out will hide the passed exception unless > you remember to attach it as $previous > - return a new exception, which according to the current RFC text will > be completely ignored (is "throw $e" supposed to say "throw $__ret"?) > > > It does seem like it would be more straightforward to have the return > value be "void", and leave it to the implementation to throw or not. > > In practice, as you say, "throw unchanged" will be common, but unless > that's the behaviour of a *null* return (i.e. the default if not opted > out), "throw $e;" seems the natural boilerplate for that.
Since this seems like a contentious area, let me go back to first principles and try to explore the problem space. (This is an essay; meaning I don't know where it's going to end up yet as I write this.) ## The problem space A context manager may exit in one of two conditions: Success or Failure. There are three types of code that a CM may want to run, but not always all of them: * Code that happens on success. * Code that happens on failure. * Code that happens on both. In the case of Success, there is no value to propagate. In the case of Failure, there is a value (exception) to conditionally propagate, but the default case should be propagate. The CM may have its own error, in which case it will want to propagate that error (as a throwable). Would a CM ever want to wrap-and-rethrow a lower-level exception, rather than just passing it on itself? I suppose it's possible, though I'm not sure of a specific example off hand. ## Python's answer The Python answer is a single `__exit__` method, which may be passed optional exception information. In Python, not returning anything is equivalent to returning null, which is falsy, so "return true to stop propagation or do nothing to continue propagation" is reasonable and ergonomic. That is not the case in PHP, however; a function with a typed return MUST have a `return` statement in it, and it's not immediately obvious to a user what true-vs-false will do. (Does true mean "yes propagate" or "yes suppress"?) It also makes the return value meaningless in the success case, which is not immediately obvious. So mimicking Pythong in this case is not a viable approach. ## Basic structure In PHP, we could have the three different code paths in one, two, or three methods. The three method approach would be something like: ``` public function contextSuccess() { // Do stuff only on a success case. } public function contextFail(Throwable $e): bool { // Do stuff only on a failure case, you must return a value. } public function contextExit() { // Do stuff in any case. } ``` That approach has 2 problems: One, contextExit() must then be called either before or after the success/fail callback, always, which may not support the desired cleanup process. Two, if all three are on the CM interface then they all must be implemented, even if there's nothing for them to do. That's bad ergonomics. (Side note: interface-default-methods would help a ton here.) The second point could be resolved by making them all magic methods rather than an interface, but that brings with it all the lack-of-introspection challenges of magic methods. The two method approach would be: ``` public function contextSuccess() { // Do stuff only on a success case. // Do common stuff here. } public function contextFail(Throwable $e): bool { // Do stuff only on a failure case, you must return a value. // Do common stuff here, redundantly. } ``` Or alternatively, call out to a separate common method from both. This would probably work, but again has two problems: One, it makes common actions harder to do, as it requires either redundancy or another method (which may not always be viable in context). Two, the same "must define both of them even if you don't care" problem exists. (Again, interface-default-methods would solve this.) The single method approach is what the RFC currently proposes: ``` public function contextExit(?Throwable $e) { // Do whatever you want, in whatever order, and if ($e === null) to differentiate success/failure. } ``` This approach resolves both issues of the previous models, but creates one more: the exit condition (return, throw, etc.) from this method is quite complex: - In a Success case, there is no exit condition (return and continue) - In a Failure case, there is a binary exit condition (propagate or suppress) - In the case contextExit() itself has a failure, it would need to throw its own exception, which implies suppressing the original. I do believe the single-method approach is the least-bad, if we can resolve the exit condition question. ## Ways of handling a single method's returns Possible ways to do so off the top of my head, in no particular order: - Return True to suppress, return False to propagate, return Null on success, throw on CM error. (This is what earlier versions of the RFC had.) Pro: Clear delineation for each pathway. Con: Needlessly complex in practice and not self-documenting. Doesn't differentiate between CM exception and underlying exception. ``` public function exitContext(?Throwable $e): ?bool { if ($e) { // Error cleanup } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup return $e === null; } ``` - Same as previous, but use enums. Pro: more self-documenting. Con: Still needlessly complex, now more verbose, too! Doesn't differentiate between CM exceptions and underlying exceptions. Returning one of the Failure case values on a Success case would, uh, just ignore it? That's not great. ``` public function exitContext(?Throwable $e): ?bool { if ($e) { // Error cleanup } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup if ($e) { if (something) { return CMResult::Propagate; } return CMResult::Suppress; } else { return CMResult::Success; } } ``` - Return an exception to cause it to throw, or null to not throw. This folds the return value into a single line in most cases. (This is what the RFC says right now.) Pro: Ergonomically very convenient. Con: `return $e` and `throw $e` become effectively the same thing, so it's not clear when you'd use one or the other. Doesn't differentiate between CM exception and underlying exception. ``` public function exitContext(?Throwable $e): ?Throwable { if ($e) { // Error cleanup } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup return $e; } ``` - As Rowan suggested, void return, only propagate on throw. Pro: Folds different pathways together in a natural way. Con: The most common case (propagate exception) is the one that requires additional work, not the rare case (not propagating), so I can see it being very common for people to forget to rethrow. Doesn't differentiate between CM exception and underlying exception. ``` public function exitContext(?Throwable $e): void { if ($e) { // Error cleanup throw $e; } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup } ``` - Follow event-dispatcher patterns, like PSR-14, and call a built-in method to prevent propagation. Pro: In the typical case where you want to allow propagation, there's literally nothing to do. That makes the common case very ergonomic. Con: This would necessitate either a ContextManager base class instead of interface, or some black magic where adding the interface magically adds this method. (Side note: interface-default-methods would probably help here.) ``` public function exitContext(?Throwable $e): void { if ($e) { // Error cleanup throw $e; } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup // $e will get propagated unless this is called. $this->stopPropagation(); } ``` - Totally wild thought: throw a special "don't throw anything else" exception, which gets special handling. Pro: In the typical case where you want to allow propagation, there's literally nothing to do. That makes the common case very ergonomic. Con: Throwing an exception to prevent an exception from being thrown is just... weird. ``` public function exitContext(?Throwable $e): void { if ($e) { // Error cleanup } else { // Success cleanup } // Oops, something went wrong. throw CMException(); // Common cleanup // If this line is missing, $e gets rethrown. throw new StopPropagationException(); } ``` None of these allow differentiating at runtime between a CM error and an underlying error. In most it could be differentiated in static analysis, but not at runtime. Whether or not that is a problem is, I think, an open question. The Python PEP for context managers suggests that if one cares, it's possible to avoid `using` and call it manually, allowing for that differentiation. If we use the `return $e` approach, a manual/higher-order CM could make the differentiation by calling the CM methods itself, rather than relying on `using`. ``` try { $cm = new SomeCM(); $cv = $cm->enterContext(); // Do code here. $e = $cm->exitContext($e); if ($e !== null) { // body failed, exit success } } catch (\Throwable $e) { // body failed, exit failed too } ``` However, the whole point of `using` blocks is to not need to do that, so if it's a common need, that would be highly sub-optimal. It also wouldn't be available in the other approaches. ## Conclusion I think a key question to answer here is: Do we care to differentiate between CM errors and underlying errors? If not being able to do so is a non-issue, or small enough that we don't care, then we have more options. I'm not sure which of the last 3 I like most/dislike least: "always rethrow", "call method to suppress", "throw special to suppress". They all have pros and cons. If we do care, then we may need to adapt the materialized code and expand the syntax in some way to allow for it. I'm not sure yet what that would look like. I will stop here, however, and ask for input from the audience. (Not just the regulars in this thread of late, but all of you reading this.) Including if you have an alternate approach to the three listed above that would have notably fewer cons. --Larry Garfield

Tim Düsterhus

125 days ago
Hi Am 2026-04-22 20:28, schrieb Larry Garfield:
> ## Ways of handling a single method's returns > > Possible ways to do so off the top of my head, in no particular order:
I don't currently have the time to digest this email in detail, but I wanted to mention an alternative you didn't before I forget: Making the exception an in-out parameter. That would be functionally similar to a return value, but more strongly default to “don't make a change”, because “doing nothing” will just work. i.e. public function exitContext(?\Throwable &$e): void { // Assign null to suppress. $e = null; } Best regards Tim Düsterhus

Rob Landers

125 days ago
On Wed, Apr 22, 2026, at 20:28, Larry Garfield wrote:
> I think a key question to answer here is: Do we care to differentiate between CM errors and underlying errors?
I don't think we need to differentiate at the CM level. Suppression is a policy decision that belongs to the caller, not the context manager. The CM's job is cleanup: rollback the transaction, close the file, release the lock, etc. Whether the exception continues propagating after that is the caller's call, and `try using` already provides exactly that: try using ($db->transaction() => $conn) { $conn->execute('INSERT INTO orders ...'); } catch (ValidationException $e) { // Caller chooses to suppress this here } That makes `exitContext()` simple: return void, do your cleanup, and get out. If cleanup fails, it throws naturally, and the desugared form can chain the original exception as `$previous` so the root cause isn't lost. If the caller wants to suppress or differentiate, they already have `try using` for exactly that. It's worth noting that every example in the RFC (database transactions, file locks, error handler swaps, async scopes) does cleanup and propagates. None of them actually need the power to suppress. If a context manager wants to give callers a clean exception hierarchy to catch against, it can wrap underlying exceptions in their own types during cleanup. That's just normal exception design, no special syntax required. — Rob

Larry Garfield

125 days ago
On Wed, Apr 29, 2026, at 9:30 AM, Rob Landers wrote:
> On Wed, Apr 22, 2026, at 20:28, Larry Garfield wrote: >> I think a key question to answer here is: Do we care to differentiate between CM errors and underlying errors? > > I don't think we need to differentiate at the CM level. Suppression is > a policy decision that belongs to the caller, not the context manager. > The CM's job is cleanup: rollback the transaction, close the file, > release the lock, etc. Whether the exception continues propagating > after that is the caller's call, and `try using` already provides > exactly that: > > try using ($db->transaction() => $conn) { > $conn->execute('INSERT INTO orders ...'); > } catch (ValidationException $e) { > // Caller chooses to suppress this here > } > > That makes `exitContext()` simple: return void, do your cleanup, and > get out. If cleanup fails, it throws naturally, and the desugared form > can chain the original exception as `$previous` so the root cause isn't > lost. If the caller wants to suppress or differentiate, they already > have `try using` for exactly that. > > It's worth noting that every example in the RFC (database transactions, > file locks, error handler swaps, async scopes) does cleanup and > propagates. None of them actually need the power to suppress. If a > context manager wants to give callers a clean exception hierarchy to > catch against, it can wrap underlying exceptions in their own types > during cleanup. That's just normal exception design, no special syntax > required. > > — Rob
Just to make sure I'm following you, you're arguing that a CM should not have any way at all to suppress an exception? I don't think I'd agree with that, personally. Even if it's a rare case, I do believe it's a feature that should remain. --Larry Garfield

Rob Landers

124 days ago
On Wed, Apr 29, 2026, at 17:04, Larry Garfield wrote:
> On Wed, Apr 29, 2026, at 9:30 AM, Rob Landers wrote: > > On Wed, Apr 22, 2026, at 20:28, Larry Garfield wrote: > >> I think a key question to answer here is: Do we care to differentiate between CM errors and underlying errors? > > > > I don't think we need to differentiate at the CM level. Suppression is > > a policy decision that belongs to the caller, not the context manager. > > The CM's job is cleanup: rollback the transaction, close the file, > > release the lock, etc. Whether the exception continues propagating > > after that is the caller's call, and `try using` already provides > > exactly that: > > > > try using ($db->transaction() => $conn) { > > $conn->execute('INSERT INTO orders ...'); > > } catch (ValidationException $e) { > > // Caller chooses to suppress this here > > } > > > > That makes `exitContext()` simple: return void, do your cleanup, and > > get out. If cleanup fails, it throws naturally, and the desugared form > > can chain the original exception as `$previous` so the root cause isn't > > lost. If the caller wants to suppress or differentiate, they already > > have `try using` for exactly that. > > > > It's worth noting that every example in the RFC (database transactions, > > file locks, error handler swaps, async scopes) does cleanup and > > propagates. None of them actually need the power to suppress. If a > > context manager wants to give callers a clean exception hierarchy to > > catch against, it can wrap underlying exceptions in their own types > > during cleanup. That's just normal exception design, no special syntax > > required. > > > > — Rob > > Just to make sure I'm following you, you're arguing that a CM should not have any way at all to suppress an exception? I don't think I'd agree with that, personally. Even if it's a rare case, I do believe it's a feature that should remain.
I'd argue CMs are mechanisms, not policies. They encode setup and teardown. Suppression is an error-handling policy decision that should be visible at the call site, not mixed with the concerns of setting up and tearing down resources. If CM's could suppress arbitrary exceptions, from a developer stepping over the code, they'd see the code randomly appear to jump out of the using block after a suppressed exception ... at seemingly arbitrary points. There would be no way to trust what you were reading without having the CM's code in front of you as well. — Rob

Tim Düsterhus

124 days ago
Hi Am 2026-04-30 09:42, schrieb Rob Landers:
> I'd argue CMs are mechanisms, not policies. They encode setup and > teardown. Suppression is an error-handling policy decision that should > be visible at the call site, not mixed with the concerns of setting up > and tearing down resources.
I agree here and noted something similar in the “RAII vs Context Manager” thread: https://news-web.php.net/php.internals/129463 (last paragraph). Best regards Tim Düsterhus

Côme Chilliet

118 days ago
Le 22 avril 2026 20:28:15 GMT+02:00, Larry Garfield <larry@garfieldtech.com> a écrit :
>I will stop here, however, and ask for input from the audience. (Not just the regulars in this thread of late, but all of you reading this.) Including if you have an alternate approach to the three listed above that would have notably fewer cons. > >--Larry Garfield
I prefer the void return and throw if needed approach, it looks way more understandable. I was confused by that part when reading the RFC and really surprised that returning an Throwable on success is ignored, which is not clear at all when reading the interface. The in-out parameter works too but is a bit weirder, and makes it unclear what happens if exitContext throws. It's also unclear to me in the current desugarized version what happens when exitContext throws, the reset of the context var does not happen ? There is nothing to handle that. Côme

Larry Garfield

118 days ago
On Wed, May 6, 2026, at 8:31 AM, Côme Chilliet wrote:
> Le 22 avril 2026 20:28:15 GMT+02:00, Larry Garfield > <larry@garfieldtech.com> a écrit : >>I will stop here, however, and ask for input from the audience. (Not just the regulars in this thread of late, but all of you reading this.) Including if you have an alternate approach to the three listed above that would have notably fewer cons. >> >>--Larry Garfield > > I prefer the void return and throw if needed approach, it looks way > more understandable. I was confused by that part when reading the RFC > and really surprised that returning an Throwable on success is ignored, > which is not clear at all when reading the interface.
By which you mean the "if you do nothing, the exception is swallowed" approach? (IE, more work in the common case.) My reluctance there is that it will become really easy to forget to propagate. public function exitContext(?Throwable $e) { fclose($this->fp); } That seems like it should be all you need, but it will also silently swallow any errors, so whatever code uses this context manager won't know if it was successful or not. That seems not-great to me.
> The in-out parameter works too but is a bit weirder, and makes it > unclear what happens if exitContext throws. > > It's also unclear to me in the current desugarized version what happens > when exitContext throws, the reset of the context var does not happen ? > There is nothing to handle that. > > Côme
We'll have to clean up the desugared versions once we decide what they should actually be. :-) There's probably a bug in there at the moment. --Larry Garfield

Larry Garfield

113 days ago
On Wed, Apr 22, 2026, at 1:28 PM, Larry Garfield wrote:
> ## Conclusion > > I think a key question to answer here is: Do we care to differentiate > between CM errors and underlying errors? If not being able to do so is > a non-issue, or small enough that we don't care, then we have more > options. I'm not sure which of the last 3 I like most/dislike least: > "always rethrow", "call method to suppress", "throw special to > suppress". They all have pros and cons. > > If we do care, then we may need to adapt the materialized code and > expand the syntax in some way to allow for it. I'm not sure yet what > that would look like. > > I will stop here, however, and ask for input from the audience. (Not > just the regulars in this thread of late, but all of you reading this.) > Including if you have an alternate approach to the three listed above > that would have notably fewer cons. > > --Larry Garfield
Coming back here with another option. Inspired by Bob's earlier message, I spent a little time noodling with going all in on generator-based context managers, aka "single-function" CMs. My noodling is here: https://gist.github.com/Crell/a599423f9e7c312650a45b0bcbafa473 It shows a few of the examples from the RFC converted to single-function generators with attribute tags. I also imagined what it would look like if we were to special case an object with __invoke() and an attribute tag, so that it could be autoloaded. And that in turn suggested going back to an interface but with a single method that must be a generator. All of those are shown in the gist. I think there is potential here. I list some of the pros/cons at the bottom. The big win is that it largely resolves the return-value/suppression question; the CM author is writing their own try-catch-finally anyway, so they can make it work however the heck the want it to using existing syntax. The downside is largely that it opens up some other questions about other parts of generators and what they do; Arnaud pointed out that simply forbidding those (returning from the CM generator or yielding with a key) until we decide what they should do is a viable option, and if the consensus is to do that I'd be OK with it. We haven't tried implementing any of the above yet; we want to get feedback on how folks feel about it as an alternative to the two-method approach. Please share your thoughts. --Larry Garfield