[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