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

Reply via email to