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