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

Reply via email to