# [RFC] Context Managers

- Thread: https://externals.io/message/129077
- Original archive: https://news-web.php.net/php.internals/129077
- Started by Larry Garfield on 2025-11-04T20:13:18+00:00
- Messages: 102
- RFC: Context Managers (Under discussion) — https://externals.io/rfc/context-managers.md
- Page 1 of 5: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/129077.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #129077 — Larry Garfield — 2025-11-04T20:13:18+00:00

URL: https://externals.io/message/129077#129077

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

---

## #129079 — Edmond Dantes — 2025-11-05T05:40:10+00:00

URL: https://externals.io/message/129077#129079 · In reply to: #129077

Hello.

Thank you for the RFC.
An excellent tool for a language that supports interfaces.

--
Ed

---

## #129080 — Paul Dragoonis — 2025-11-05T06:29:29+00:00

URL: https://externals.io/message/129077#129080 · In reply to: #129077

> 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!



> --
>   Larry Garfield
>   larry@garfieldtech.com

---

## #129081 — Valentin Udaltsov — 2025-11-05T06:45:32+00:00

URL: https://externals.io/message/129077#129081 · In reply to: #129079

ср, 5 нояб. 2025 г., 09:42 Edmond Dantes <edmond.ht@gmail.com>:

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

Hi, Larry!

Have you considered returning enum instead of ?bool? It would have a clear
self explanatory meaning.

—

> Valentin

---

## #129082 — Edmond Dantes — 2025-11-05T07:06:55+00:00

URL: https://externals.io/message/129077#129082 · In reply to: #129081

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

---

## #129083 — Deleu — 2025-11-05T07:38:58+00:00

URL: https://externals.io/message/129077#129083 · In reply to: #129077

> 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

> <larry@garfieldtech.com

---

## #129086 — Davey Shafik — 2025-11-05T09:25:14+00:00

URL: https://externals.io/message/129077#129086 · In reply to: #129077

> 
> 
> ﻿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

---

## #129093 — Juris Evertovskis — 2025-11-05T20:30:59+00:00

URL: https://externals.io/message/129077#129093 · In reply to: #129083

> 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

---

## #129094 — Niels Dossche — 2025-11-05T21:02:57+00:00

URL: https://externals.io/message/129077#129094 · In reply to: #129077

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

---

## #129095 — Larry Garfield — 2025-11-05T22:37:42+00:00

URL: https://externals.io/message/129077#129095 · In reply to: #129077

> 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:



> 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.




> Have you considered returning enum instead of ?bool? It would have a 
> clear self explanatory meaning.

We have discussed that a bit, actually.  The main concern is usability.  The typical case will be to allow exceptions to propagate.  If, say, a TypeError gets thrown 4 function calls down, you probably do want that to propagate to your top level handler, but still want to rollback your transaction or close your file or whatever.  So the typical case should be easy, hence why we said `null` means the default behavior.  And since `null` is falsy`, that fits neatly into a ?bool return; that is also what Python uses.

With an enum, you'd have a much longer thing to type, plus it's less self-evident what no-return means.  Ie, you'd have:

function exitContext(?Exception $e): ContextResult
{
  if ($e) {
    $this->conn->rollback();
    // This line becomes required.
    return ContextResult::Propagate;
  }
  
  $this->conn->commit();
  return ContextResult::Done; // Or, eh, what?
}

And not returning becomes a type error.

Alternatively, we could assume null implies one of the other cases; but as shown above, there's still the issue that the return type is only meaningful in case of an exception, so it's unclear how that interacts.

We're still open to discussion here.  It also would play into the outstanding question of `with` as an expression.




> Out of curiosity, what happens if GOTO is used inside a context block 
> to jump away from it?

That would be a success case, just like break or return.  Basically anything other than an exception is a success case.  (That said, please don't use Goto. :-) )

> Could the RFC clarify the relation between Context and switch/case? I 
> thought it was really odd that something that triggers a warning on 
> switch/case is being introduced into a brand new language construct 
> basically creating the possibility for new code to fall into the same 
> trap as opposed to avoiding it in the first place. Specially a 
> construct like switch/case that has been in decline for over a decade 
> and ever since match came out on 8.0, switch case is practically 
> deprecated without actually being deprecated yet. What’s the 
> importance/relevance of being consistent with it?

`break` and `continue` are interesting keywords.  (In the "may you live in interesting times" sense.)  Sometimes they have the same effect, if a control structure is non-looping.  Or they may have different effects in case it is.  The main non-looping case is `switch`, where for reasons that were before my time the decision was made to deprecate `continue` in favor of just supporting `break`.  However, blocking it entirely is a problem, because that would change where `continue 2` would go (as `switch` would be removed as a "level" that it could go to).  It is kind of a mess.

`with` is a non-looping control structure, and thus it seems logical to be consistent with other non-looping control structures.  But, as noted, the other non-looping control structure is a mess. :-)  Therefore, we get to choose between "a consistent mess" and "an inconsistent non-mess in one place and a mess in another."

Neither is a fantastic option.  We're open to both, depending on what the consensus is.

> While we’re at it, do we really need break; statements inside context 
> blocks? If you want out you can:
>
> - return
> - throw

Both of those exit the function the `with` statement is in, which is not always desireable.

> In the case of a nested block (break 2;) where I don’t want to wrap the 
> entire thing in try/catch, it seems like a GOTO out of it would be more 
> meaningful with text-based identifiers rather than number-based, which 
> leads to my first question (although I was more curious than actually 
> making an argument for it because I would rather avoid nested with as 
> much as possible).


Because Goto was added to PHP as a troll, and not a feature you should actually use in production code 99.999% of the time. :-)



> I really like this RFC but have a couple of things to discuss:
>
> - automatically re-throwing exceptions: I think that this behavior, 
> especially with a boolean return value deciding if it happens or not is 
> not intuitive. I think a better approach is to do nothing with the 
> exception and let the user re-throw it if desired. I can't think of 
> anywhere else we re-throw exceptions unless the user indicates 
> otherwise. I'd rather leave the return value for return values; we 
> could expand this allow access to the return value like: with (foo() as 
> $foo return $bar) { }, and $bar would be set to null on void returns.

Using the return value of exitContext() as the result of a "with expression" is something we are considering.

However, we're modeling on Python (the most robust such functionality we are aware of), and they rethrow by default.  Essentially, the concept is that exitContext() (and it's Python equivalent magic method), is mostly a `finally` block, not a `catch` block.  `finally` blocks do propagate exceptions.  In practice, many exitContext() methods will not need to differentiate; they'll just close a file or whatever and move on with life, which is why you would want an exception to propagate.  The inclusion of the exception parameter makes it a sort of combined catch/finally, so it has some behavior of each.

Another option we kicked around was splitting it into two methods; catchContext(Throwable $e) and exitContext().  However, that creates two other problems:

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

---

## #129096 — Bob Weinand — 2025-11-05T22:38:58+00:00

URL: https://externals.io/message/129077#129096 · In reply to: #129077

Hey Larry, Tim, Seifeddine and Arnauld,


> Arnaud and I would like to present another RFC for consideration: Context Managers.
>
> https://wiki.php.net/rfc/context-managers
>
> You'll probably note that is very similar to the recent proposal from Tim and Seifeddine.  Both proposals grew out of casual discussion several months ago; I don't believe either team was aware that the other was also actively working on such a proposal, so we now have two.  C'est la vie. :-)
>
> Naturally, Arnaud and I feel that our approach is the better one.  In particular, as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters.  It also does not allow differentiating between a success or failure exit condition, which for many use cases is absolutely mandatory (as shown in the examples in the context manager RFC).
>
> The Context Manager proposal is a near direct port of Python's approach, which is generally very well thought-out.  However, there are a few open questions as listed in the RFC that we are seeking feedback on.
>
> Discuss. :-)

I've been looking at both RFCs and I don't think either RFC is good 
enough yet.


As for this RFC:

It makes it very easy to not call the exitContext() method when calling 
enterContext() manually. The language (obviously) doesn't prevent 
calling enterContext() - and that's a good thing. But also, it will not 
enforce that exitContext() gets ever called (and it also cannot, 
realistically).

Thus, we have a big pitfall, wherein APIs may expect enterContext() and 
exitContext() to be called in conjunction, but users don't - with 
possibly non-trivial side-effects (locks not cleared, transactions not 
committed etc.). Thus, to be safe, implementers of the interface will 
also likely need the destructor to forward calls to exitContext() as 
well. But it's an easy thing to forget - after all, the *intended* usage 
of the API just works. Why would I think of that, as an implementer of 
the interface, before someone complains?

Ultimately you definitely will need the capability of calling 
enterContext() and exitContext() manually too (i.e. restricting that is 
not realistic either), as lifetimes do not necessarily cleanly nest - as 
a trivial example, you might want to obtain access to a handle which is 
behind a lock. You'll have to enter the context of the lock, enter the 
context of the handle, and close the lock (because more things are 
locked behind that lock, including the handle). ... But you don't 
necessarily want the hold on the lock to outlive the inner handle. In 
short: The proposed approach only allows nesting contexts, but not 
interleaving them.


Further, calling `with()` twice on an object is quite bad in general. But 
it might easily happen - you have a function which wants a transaction. 
e.g. function writeSomeData(DatabaseTransaction $t) { with ($t) { 
$t->query("..."); } }. A naive caller might think, DatabaseTransaction 
implements ContextManager ... so let's wrap it: with($db->transaction() 
as $t) { writeSomeData($t); }. But now you are nesting a transaction, 
which may have unexpected side effects - and the code probably not 
prepared to handle it. So, you have to add yet another safeguard into 
your implementation: check whether enterContext() is only active once. 
... Or, maybe a caller assumes that $t = $db->transaction(); with ($t) { 
$t->query("..."); } with ($t) { $t->query("..."); } is fine - but the 
implementation is not equipped to handle multiple calls to enterContext().


Additionally, I would expect implementers to want to provide methods, 
which can be called while the context is active. However, it's not 
impossible to call these methods without wrapping it into `with()` or 
calling the enterContext() method explicitly. One more failure mode, 
which needs handling.
Like for example, calling $t->query() on a transaction without starting it.


I don't like that design, which effectively forces you to put safety 
checks for all but the simplest cases onto the ContextManager 
implementation.
And it forces the user to recognize "this returned object 
DatabaseTranscation actually implements ContextManager, thus I should 
put it into `with()` and not immediately call methods on it". (A problem 
which the use() proposal from Tim does not have by design.)


The choice of adding the exception to the exitContext() is interesting, 
but also very opinionated:

- It means, that the only way to abort, in non-exceptional cases, is to 
throw yourself an exception. And put a try/catch around the `with()` {} 
block. Or manually use enterContext() & exitContext() - with a fake "new 
Exception" essentially.
- Maybe you want to hold a transaction, but just ensure that everything 
gets executed together (i.e. atomicity), but not care about whether 
everything actually went through (i.e. not force a rollback on 
exception). You'll now have to catch the exception, store it to a 
variable, use break and check for the exception after the with block. 
Or, yes, manually using enterContext() and exitContext().


It feels like `with()` is designed to be covering 70% of the use cases, 
with a load of hidden pitfalls and advanced usage requiring manual 
enterContext() and exitContext() calls. It's not a very good solution.


As to the destructors (and also in reply to that other email from 
Arnauld talking about PDO::disconnect() etc.):

It's already possible today to have live objects which are already 
destructed. It's extremely common to have in shutdown code. It's 
sometimes a pain, I agree. But it's an already known pain, and an 
already handled pain in a lot of code.
If your object only knows "create" and "destruct", there's no way for a 
double enterContext() (nested or consecutive) situation to ever happen. 
(Well, yes, you *could* theoretically manually call __destruct(), but 
why would you ever do that?)


Last thing - proper API usage forces you to use that construct.


To the use() proposal from Tim:

This proposal makes it very simple to inadvertently leak the use()'d 
value. I don't think the current proposed form goes far enough.

However we could decide to force-destruct an object (just like we do in 
shutdown too). It's just one single flag for child methods to check as 
well - the object is either destructed or not. We could also trivially 
prohibit nested use() calls by throwing an AlreadyDestructedError when 
an use()'d and inside destructed object crosses the use() boundary.

The only disadvantage is that there's no information about thrown 
exceptions. I.e. you cannot add a default behaviour of "on exception, 
please do this", like rolling transactions back. But:
- Is it actually a big problem? Where is the specific disadvantage over 
simply $db->transaction(function($t) { /* do stuff */ }); - where the 
call of the passed Closure can be trivially wrapped in try/catch.
- If yes, can we e.g. add an interface ExceptionDestructed { public bool 
$destructedDuringException; }? Which will set that property if the 
property is still undefined - to true if the destructor gets triggered 
inside ZEND_HANDLE_EXCEPTION. To false otherwise. And, if an user 
desires to manually force success/failure handling, he may set 
$object->destructedDuringException = true; himself as a very simple - 
one-liner - escape hatch.


The use() proposal is not a bad one, but I feel like requiring the RC to 
drop to zero first, misses a bit of potential to save users from mistakes.
The other nice thing about use() is that it's optional. You don't have 
to use it. You use it if you want some scoping, otherwise the scope is 
simply the function scope.


To both proposals:

It remains possible by default to call methods on the object, after 
leaving the with or use block. So some checking on methods for a safe 
API is probably still required.

I don't think it's possible to solve that problem at all with the 
ContextManager RFC, except manual checking by the implementer in every 
single method. But it's possibly possible to solve it with the use() RFC 
in orthogonal ways - like a #[ProhibitDestructed] attribute, which can 
be added onto a class (making it apply to all methods) or a specific 
method and causes method calls to throw an exception when the object is 
destructed.
Which is possible to provide by the language, as the language knows 
about whether objects are already destructed, unlike e.g. the 
ContextManager, where it would be object state, which has to be 
maintained by the user.


TL;DR: ContextManagers are a buttload of pitfalls. use() is probably 
better, with much less inherent problems. And with the remaining 
problems of the proposal being actually solvable.


Thanks,

Bob

---

## #129098 — Bob Weinand — 2025-11-05T22:53:44+00:00

URL: https://externals.io/message/129077#129098

Oops, pressed send too fast:


> Last thing - proper API usage forces you to use that construct.

I wanted to add that you have to use the construct to do it properly. 
But it's easy to either not be aware of `with()` and just see 
enterContext(), because that makes it work. Leaving the context is not 
strictly required for some use cases where you'd use the ContextManager.

Also, I have a lot of experience with it from C#, and it ... sucks. It's 
annoying. I forget about using() all the time. It's crappy design.
It also only has IDisposable, and does not require enterContext() first. 
So yes, at least proper implementations of the ContextManager will 
probably do the effort to blow up in your face if you don't call 
enterContext().
But still, it's a discovery step away. It's not intuitive that it has to 
be called (or better, wrapped in with :-)) without running the code / 
looking at the actual interfaces implemented by the returned object.


The use() proposal has the big advantage, that function boundaries are 
pretty natural. You do not actually introduce cycles *that* easily, 
unless you store intermediary state (like callbacks, custom user objects).
It will work flawlessly in most cases without use(). And it's easy to 
think "this is a database transaction, I probably should wrap it in 
use()" - without having any actual knowledge of the implementation 
details of the class (like the ContextManager being implemented).


Sure, use() is not *perfect* either - unlike e.g. Rust which 
intrinsically prevents cycles from being possible at all, but it's much 
better than the `with()` with all its shortcomings.


Small addendum on the forcing destructors topic: We might want to make 
the force-destructable behaviour opt-in. I.e. by default, if you use() 
an object and it leaves scope, nothing happens, unless a 
#[ForceDestructInUse] attribute is given, to prevent accidental 
destruction of objects which very much should not be autodestructed.


Sorry for splitting into two mails...

Bob

---

## #129099 — Rowan Tommins [IMSoP] — 2025-11-05T23:02:54+00:00

URL: https://externals.io/message/129077#129099 · In reply to: #129095

> `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]

---

## #129100 — Rowan Tommins [IMSoP] — 2025-11-05T23:08:20+00:00

URL: https://externals.io/message/129077#129100 · In reply to: #129077

> 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]

---

## #129102 — Rowan Tommins [IMSoP] — 2025-11-05T23:27:41+00:00

URL: https://externals.io/message/129077#129102 · In reply to: #129096

> 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]

---

## #129103 — Bob Weinand — 2025-11-05T23:58:52+00:00

URL: https://externals.io/message/129077#129103 · In reply to: #129102

Hey Rowan,


> 
> > […]
>
>
> 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

---

## #129114 — Rowan Tommins [IMSoP] — 2025-11-06T12:18:26+00:00

URL: https://externals.io/message/129077#129114 · In reply to: #129103

>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]

---

## #129118 — Jeffrey Dafoe — 2025-11-06T17:20:31+00:00

URL: https://externals.io/message/129077#129118 · In reply to: #129102

> 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

---

## #129152 — Rowan Tommins [IMSoP] — 2025-11-08T19:54:28+00:00

URL: https://externals.io/message/129077#129152 · In reply to: #129096

> 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]

---

## #129163 — Larry Garfield — 2025-11-09T15:01:58+00:00

URL: https://externals.io/message/129077#129163 · In reply to: #129152

> 
> > […]
>
>
> 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

---

## #129165 — Larry Garfield — 2025-11-09T15:07:40+00:00

URL: https://externals.io/message/129077#129165 · In reply to: #129103

> 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

---

## #129185 — Seifeddine Gmati — 2025-11-09T21:46:17+00:00

URL: https://externals.io/message/129077#129185 · In reply to: #129096

>
> Hey Larry, Tim, Seifeddine and Arnauld,
>
> 
> > […]
>
> 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.

---

## #129187 — Edmond Dantes — 2025-11-10T07:13:27+00:00

URL: https://externals.io/message/129077#129187 · In reply to: #129096

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

---

## #129195 — Derick Rethans — 2025-11-11T11:31:15+00:00

URL: https://externals.io/message/129077#129195 · In reply to: #129095

> 
> 
> > […]
> 
> 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

---

## #129201 — Rowan Tommins [IMSoP] — 2025-11-11T20:20:02+00:00

URL: https://externals.io/message/129077#129201 · In reply to: #129185

> 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]

---

Next page: https://externals.io/message/129077.md?page=2
