[PHP-DEV] [RFC] Context Managers

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

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 php.internals: Re: Examples comparing Block Scoped RAII and Context Managers, `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. :slight_smile: 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

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

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. :slight_smile: 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 (Working with Context Managers in C# | Useful Codes). Context Managers are also used in Java, again for similar but not as robust functionality (Working with Context Managers in Java | Useful Codes).

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

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

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:

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

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

// ...
$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]

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

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.

PHP: 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. :slight_smile:

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

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

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.

PHP: 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. :slight_smile:

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

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

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.

PHP: 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. :slight_smile:

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

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

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: php.internals: Re: [RFC] Context Managers. 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())) { }

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:
php.internals: Re: [RFC] Context Managers. 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

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: [RFC] Deprecate and remove continue targeting switch - Externals

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]

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:
php.internals: Re: [RFC] Context Managers. 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

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

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]

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

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]

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

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

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

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


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