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