[PHP-DEV] [RFC] Context Managers

On Wed, Nov 5, 2025, at 5:58 PM, Bob Weinand wrote:

You are right, I missed that there's an extra layer of nesting inside this.
I think the DatabaseTransaction example put me on the wrong thought path
because it just returned the connection it came from instead of a
dedicated nested Transaction object. (The enterContext method in that
example ought to include a startTransaction call.)

However, I still think the proposed approach is dangerous with respect
to forgetting the exitContext() call. When using manual handling. But
yes, I agree, that's a much more manageable concern.
And the onus of handling duplicate enterContext() and multiple
exitContext() calls still lies on the implementer. The RFC does zero
effort at addressing this.

Thanks,
Bob

Bob, you seem to be focused on the "manual call" case. That... is not a case that exists. I cannot think of any situation where a user creates a context manager and then calls enter/exit context themselves that isn't a "stop that, you're doing it wrong" situation.

By the same token, you *can* implement Iterator and then call current() and next() and valid() yourself... but odds are you're doing something wrong, and it's very easy to screw things up if you call those methods in the wrong order or too many times or whatever. If you implement Iterator, you're supposed to use it with foreach(). Doing anything else with it is "well technically that maybe works, but please don't."

As Rowan noted, calling __construct() or __destruct() yourself is also possible, and can cause things to go sideways, and if they do then it's your own damned fault, don't do that.

The same is true here. Someone manually calling enterContext and then not calling exitContext() is... simply not a use case that matters, because there's no good reason to ever do so. The only effort the RFC needs to make in this regard is say "don't do that."

--Larry Garfield

On Wed, 5 Nov 2025 at 23:39, Bob Weinand <bobwei9@hotmail.com> wrote:

Hey Larry, Tim, Seifeddine and Arnauld,

On 4.11.2025 21:13:18, 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:

I've been looking at both RFCs and I don't think either RFC is good
enough yet.

As for this RFC:

It makes it very easy to not call the exitContext() method when calling
enterContext() manually. The language (obviously) doesn't prevent
calling enterContext() - and that's a good thing. But also, it will not
enforce that exitContext() gets ever called (and it also cannot,
realistically).

Thus, we have a big pitfall, wherein APIs may expect enterContext() and
exitContext() to be called in conjunction, but users don't - with
possibly non-trivial side-effects (locks not cleared, transactions not
committed etc.). Thus, to be safe, implementers of the interface will
also likely need the destructor to forward calls to exitContext() as
well. But it's an easy thing to forget - after all, the *intended* usage
of the API just works. Why would I think of that, as an implementer of
the interface, before someone complains?

Ultimately you definitely will need the capability of calling
enterContext() and exitContext() manually too (i.e. restricting that is
not realistic either), as lifetimes do not necessarily cleanly nest - as
a trivial example, you might want to obtain access to a handle which is
behind a lock. You'll have to enter the context of the lock, enter the
context of the handle, and close the lock (because more things are
locked behind that lock, including the handle). ... But you don't
necessarily want the hold on the lock to outlive the inner handle. In
short: The proposed approach only allows nesting contexts, but not
interleaving them.

Further, calling with() twice on an object is quite bad in general. But
it might easily happen - you have a function which wants a transaction.
e.g. function writeSomeData(DatabaseTransaction $t) { with ($t) {
$t->query("..."); } }. A naive caller might think, DatabaseTransaction
implements ContextManager ... so let's wrap it: with($db->transaction()
as $t) { writeSomeData($t); }. But now you are nesting a transaction,
which may have unexpected side effects - and the code probably not
prepared to handle it. So, you have to add yet another safeguard into
your implementation: check whether enterContext() is only active once.
... Or, maybe a caller assumes that $t = $db->transaction(); with ($t) {
$t->query("..."); } with ($t) { $t->query("..."); } is fine - but the
implementation is not equipped to handle multiple calls to enterContext().

Additionally, I would expect implementers to want to provide methods,
which can be called while the context is active. However, it's not
impossible to call these methods without wrapping it into with() or
calling the enterContext() method explicitly. One more failure mode,
which needs handling.
Like for example, calling $t->query() on a transaction without starting it.

I don't like that design, which effectively forces you to put safety
checks for all but the simplest cases onto the ContextManager
implementation.
And it forces the user to recognize "this returned object
DatabaseTranscation actually implements ContextManager, thus I should
put it into with() and not immediately call methods on it". (A problem
which the use() proposal from Tim does not have by design.)

The choice of adding the exception to the exitContext() is interesting,
but also very opinionated:

- It means, that the only way to abort, in non-exceptional cases, is to
throw yourself an exception. And put a try/catch around the with() {}
block. Or manually use enterContext() & exitContext() - with a fake "new
Exception" essentially.
- Maybe you want to hold a transaction, but just ensure that everything
gets executed together (i.e. atomicity), but not care about whether
everything actually went through (i.e. not force a rollback on
exception). You'll now have to catch the exception, store it to a
variable, use break and check for the exception after the with block.
Or, yes, manually using enterContext() and exitContext().

It feels like with() is designed to be covering 70% of the use cases,
with a load of hidden pitfalls and advanced usage requiring manual
enterContext() and exitContext() calls. It's not a very good solution.

As to the destructors (and also in reply to that other email from
Arnauld talking about PDO::disconnect() etc.):

It's already possible today to have live objects which are already
destructed. It's extremely common to have in shutdown code. It's
sometimes a pain, I agree. But it's an already known pain, and an
already handled pain in a lot of code.
If your object only knows "create" and "destruct", there's no way for a
double enterContext() (nested or consecutive) situation to ever happen.
(Well, yes, you *could* theoretically manually call __destruct(), but
why would you ever do that?)

Last thing - proper API usage forces you to use that construct.

To the use() proposal from Tim:

This proposal makes it very simple to inadvertently leak the use()'d
value. I don't think the current proposed form goes far enough.

However we could decide to force-destruct an object (just like we do in
shutdown too). It's just one single flag for child methods to check as
well - the object is either destructed or not. We could also trivially
prohibit nested use() calls by throwing an AlreadyDestructedError when
an use()'d and inside destructed object crosses the use() boundary.

The only disadvantage is that there's no information about thrown
exceptions. I.e. you cannot add a default behaviour of "on exception,
please do this", like rolling transactions back. But:
- Is it actually a big problem? Where is the specific disadvantage over
simply $db->transaction(function($t) { /* do stuff */ }); - where the
call of the passed Closure can be trivially wrapped in try/catch.
- If yes, can we e.g. add an interface ExceptionDestructed { public bool
$destructedDuringException; }? Which will set that property if the
property is still undefined - to true if the destructor gets triggered
inside ZEND_HANDLE_EXCEPTION. To false otherwise. And, if an user
desires to manually force success/failure handling, he may set
$object->destructedDuringException = true; himself as a very simple -
one-liner - escape hatch.

The use() proposal is not a bad one, but I feel like requiring the RC to
drop to zero first, misses a bit of potential to save users from mistakes.
The other nice thing about use() is that it's optional. You don't have
to use it. You use it if you want some scoping, otherwise the scope is
simply the function scope.

To both proposals:

It remains possible by default to call methods on the object, after
leaving the with or use block. So some checking on methods for a safe
API is probably still required.

I don't think it's possible to solve that problem at all with the
ContextManager RFC, except manual checking by the implementer in every
single method. But it's possibly possible to solve it with the use() RFC
in orthogonal ways - like a #[ProhibitDestructed] attribute, which can
be added onto a class (making it apply to all methods) or a specific
method and causes method calls to throw an exception when the object is
destructed.
Which is possible to provide by the language, as the language knows
about whether objects are already destructed, unlike e.g. the
ContextManager, where it would be object state, which has to be
maintained by the user.

TL;DR: ContextManagers are a buttload of pitfalls. use() is probably
better, with much less inherent problems. And with the remaining
problems of the proposal being actually solvable.

Thanks,

Bob

Hi Bob,

I agree with your points, especially this one:

It makes it very easy to not call the exitContext() method when calling enterContext() manually. [...] Thus, we have a big pitfall...

This is the exact reason why, in the `use()` thread, I suggested a
`Disposable` interface should **not** have an `enter()` method. The
language should just guarantee a single `dispose()` method is called
when the scope is exited (successfully or not). This completely avoids
the "unbalanced call" pitfall. SA tools can warn/error when
`dispose()` is called manually. API authors should be aware that the
resource might receive multiple `dispose()` calls and handle this
gracefully.

-----

Regarding your other valid concerns (nested calls, using objects after
the block, "leaking" the variable), I believe those are all solvable
by how the `use()` construct evolves, which is why I prefer its
foundation.

You noted:

It remains possible by default to call methods on the object, after leaving the with or use block. So some checking on methods for a safe API is probably still required.

You're right, but the `use()` proposal has a clear path to solve this,
which is far superior IMO to the manual checks required by the
`ContextManager` design. As discussed in the `use()` thread, we could
later introduce:

1. A **`Resource`** marker interface: This would tell the engine to
handle the object specially (e.g., use weak references in backtraces
to prevent accidental refcount inflation).

2. A **`Disposable`** interface: This would have the `dispose()`
logic and could (and probably should) also be a `Resource`.

Furthermore, a `Disposable` interface opens up the possibility of
being supported *outside* the `use()` construct entirely. The engine
could guarantee `dispose()` is called whenever the object goes out of
*any* scope, just like a destructor but with crucial exception
awareness:

function x() {
  $disposable = new SomeDisposable();
  return; // $disposable->dispose(null) called
}

function y() {
  $disposable = new SomeDisposable();
  throw new Error(); // $disposable->dispose($error) called
}

This would make `Disposable` a truly powerful, general-purpose RAII
mechanism, not just a feature tied to `use()`.

With these (future) additions, the `use()` construct could be enhanced
to **enforce a no-escape policy** specifically for `Resource`s. If
`use($r = get_resource())` finishes and `$r` still has references, the
engine could throw an error.

This combination would *programmatically prevent* the "use after free"
pitfall you described, rather than relying on manual checks inside
every single method.

All of those powerful safety checks can be added in the future. I
don't see why we need to cram them into the initial `use()` RFC. Its
current `__destruct`-based implementation is **already useful today**
for APIs designed to leverage RAII (like the Psl lock example). It's
normal that few APIs are designed this way now; the feature doesn't
exist yet. Psl just happens to support it because of its Hack origins.

This seems like a much safer and more extensible path.

Hello all.

It makes it very easy to not call the exitContext() method when calling
enterContext() manually

Consider the code below:


class FileContext {
    private $handle;

    public function __construct(private string $filename, private
string $mode) {}

    public function __enter() {
        $this->handle = fopen($this->filename, $this->mode);
        return $this->handle;
    }

    public function __exit(?Throwable $e = null) {
        if ($this->handle) {
            fclose($this->handle);
        }
    }
}

$ctx = new FileContext('example.txt', 'w');
$f = $ctx->__enter();

try {
    fwrite($f, "Hello world");
} finally {
    $ctx->__exit();
}

Question: what is the probability of making a mistake in this code?
What is the likelihood that a programmer will forget to call enter and exit?

...

with (new FileContext('example.txt', 'w')) as $f {
    fwrite($f, "Hello world");
}

In this case, the probability of forgetting to call enter or exit is
zero, since the language now supports this paradigm at the syntax
level.

Question: what is the probability of accidentally calling a method
instead of not calling it?

...

with (new FileContext('example.txt', 'w')) as $f {
    $f->__enter(); // Error!
    fwrite($f, "Hello world");
}

I believe that finding a clear mathematical proof in this case is impossible.
But if we look at studies on error statistics, it is the absence of a
call that is the most common problem.
(https://tomgu1991.github.io/assets/files/19compsac.pdf)

Thus, we have a big pitfall, wherein APIs may expect enterContext() and exitContext()

Is it really a big pitfall? If so, then functions like `fopen` and
`fclose` should be removed from the language altogether,
because their existence is an even bigger pitfall.

Ultimately you definitely will need the capability of calling enterContext() and exitContext() manually too (i.e. restricting that is not realistic either)

Of course, they can be restricted: make the methods private, so that
only PHP itself can call them, and they cannot be accidentally called
anywhere else outside the class.
But I don't see any convincing reasons for doing that.

The proposed approach only allows nesting contexts, but not interleaving them.

Why does this need to be done at all?
I don't know what "interleaving contexts" means in practice.
But even if they do exist, that goes beyond the scope of the current proposal.

Nevertheless, it is worth noting that studying Python bugs for this
RFC is a good thing:
Issue 29988: with statements are not ensuring that __exit__ is called if __enter__ succeeds - Python tracker - with statements are not ensuring
that __exit__ is called if __enter__ succeeds.

Also:

Total:
**Advantages of `with`:**
* better semantics than try–catch
* less control-flow code

It would be strange to demand that `with` be safer than the rest of PHP code.
That’s an odd requirement. Of course, if PHP were a compiled language,
many checks could be done at compile time.
But we have static analysis for that.

---
Best regards, Ed

On Wed, 5 Nov 2025, Larry Garfield wrote:

On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote:

> Out of curiosity, what happens if GOTO is used inside a context
> block to jump away from it?

That would be a success case, just like break or return. Basically
anything other than an exception is a success case. (That said,
please don't use Goto. :slight_smile: )

I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.

And now the big one... also in off-list discussion, Seifeddine noted
that Laravel already defines a global function named `with`:
framework/src/Illuminate/Support/helpers.php at 12.x ¡ laravel/framework ¡ GitHub

And since this RFC would require `with` to be a semi-reserved keyword
at the parser/token level, that creates a conflict. (This would be
true even if it was namespaced, although Laravel is definitely Doing
It Wrong(tm) by using an unnamespaced function.) Rendering all
Laravel deployments incompatible with PHP 8.6 until it makes a
breaking API change would be... not good for the ecosystem.

PHP owns the top level namespace. That's been the going for as long as I
can remember. It was unwise for Laravel to flaunt that rule.

1. Java uses a parenthetical block on `try` for similar functionality
(though without a separate context manager). That would look like:

try (new Foo() as $foo) {
  // ...
}
// catch and finally become optional if there is a context.

…

2. Either `use` or `using`. The semantics here would be identical to
the current `with` proposal.

IMO, the try syntax is more confusing than another overload of use.

cheers,
Derick

Then to use it, you just pass in whatever object you want:

¡¡¡

On 09/11/2025 21:46, Seifeddine Gmati wrote:

This is the exact reason why, in the `use()` thread, I suggested a
`Disposable` interface should **not** have an `enter()` method. The
language should just guarantee a single `dispose()` method is called
when the scope is exited (successfully or not). 

Remember, the Context Manager is not the same as the resource being managed, and exitContext() is not equivalent to dispose().

In fact, it occurs to me that a C#-style using() block can be written as a Context Manager with only a few lines of code:

interface Disposable {
public function dispose(): void;
}
class Disposer implements ContextManager
{
private function __construct(private Disposable $resource) {}
public static function of(Disposable $resource): ContextManager {
return new self($resource);
}
public function enterContext(): Disposable {
return $this->resource;
}
public function exitContext(): void {
$this->resource->dispose();
}
}

with(Disposer::of(new MyDisposableResource) as $foo) {
// …
$foo->whatever();
// …
} // guaranteed call to $foo->dispose() here

Which is pretty close to a direct port of C#:

using($foo = new MyDisposableResource) {
// …
$foo->whatever();
// …
} // guaranteed call to $foo->dispose() here

You're right, but the `use()` proposal has a clear path to solve this,
which is far superior IMO to the manual checks required by the
`ContextManager` design. As discussed in the `use()` thread, we could
later introduce:

1.  A **`Resource`** marker interface: This would tell the engine to
handle the object specially (e.g., use weak references in backtraces
to prevent accidental refcount inflation).

2.  A **`Disposable`** interface: This would have the `dispose()`
logic and could (and probably should) also be a `Resource`.

Neither of these is specific to the use() block. #1 is just a general language feature - as someone else pointed out, it could be similar to #[SensitiveParameter]. #2 is, as shown above, trivial to implement using a context manager, without even needing additional language support.

Furthermore, a `Disposable` interface opens up the possibility of
being supported *outside* the `use()` construct entirely. The engine
could guarantee `dispose()` is called whenever the object goes out of
*any* scope, just like a destructor but with crucial exception
awareness:

This is an interesting idea, but again doesn’t seem to have any special relationship to the use() proposal. It’s also not what C# or Hack mean by “Disposable”, so that would probably be a poor choice of name. It sounds more like an extension of the current __destruct(), which could potentially be as simple as adding a parameter to that.

With these (future) additions, the `use()` construct could be enhanced
to **enforce a no-escape policy** specifically for `Resource`s. If
`use($r = get_resource())` finishes and `$r` still has references, the
engine could throw an error.

This combination would *programmatically prevent* the "use after free"
pitfall you described, rather than relying on manual checks inside
every single method.

Unless the error happens at compile time (probably impossible in PHP’s current workflow), or crashes the application with an uncatchable error (yikes!), I don’t think it’s possible to make that guarantee. Consider this code:

try {
use($r = get_resource()) {
SomeClass::$staticVar = $r;
}
}
catch ( Throwable $e ) {
log_and_continue($e);
}
SomeClass::$staticVar->doSomething();

What is the value of SomeClass::$staticVar? I can only see three options:

  1. It is still the open resource from get_resource(); the call succeeds, but the resource has leaked

  2. It is a closed resource, and the call to doSomething() throws an “object already disposed” Error

  3. It has been forcibly unset, and the call to doSomething() throws a “method call on null” Error

(I don’t think #3 is actually possible in the current Engine, because we only track the reference count, not a reference list; but it’s at least theoretically possible.)

And that’s leaving aside the fact that a lot of resources can end up in unusable states anyway, such as a network connection being closed from the other end.

All of those powerful safety checks can be added in the future. I
don't see why we need to cram them into the initial `use()` RFC.

Requiring a “double opt-in” (the use() block and an interface) makes the feature less reliable - you can’t see at a glance if the use() block is performing the extra cleanup, or just creating a variable scope.

I think it’s better to have a specific block that can only be used with objects implementing the appropriate interface, like using+IDisposable (C# and Hack) or try+AutoCloseable (Java https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html).

If we want a general-purpose “block scoping” feature alongside that, we can discuss exactly how that should look and operate, as I replied to Tim here: https://externals.io/message/129059#129188

-- 
Rowan Tommins
[IMSoP]

On Tue, Nov 11, 2025, at 5:31 AM, Derick Rethans wrote:

On Wed, 5 Nov 2025, Larry Garfield wrote:

On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote:

> Out of curiosity, what happens if GOTO is used inside a context
> block to jump away from it?

That would be a success case, just like break or return. Basically
anything other than an exception is a success case. (That said,
please don't use Goto. :slight_smile: )

I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.

I defer to Arnaud here.

And now the big one... also in off-list discussion, Seifeddine noted
that Laravel already defines a global function named `with`:
framework/src/Illuminate/Support/helpers.php at 12.x ¡ laravel/framework ¡ GitHub

And since this RFC would require `with` to be a semi-reserved keyword
at the parser/token level, that creates a conflict. (This would be
true even if it was namespaced, although Laravel is definitely Doing
It Wrong(tm) by using an unnamespaced function.) Rendering all
Laravel deployments incompatible with PHP 8.6 until it makes a
breaking API change would be... not good for the ecosystem.

PHP owns the top level namespace. That's been the going for as long as I
can remember. It was unwise for Laravel to flaunt that rule.

Yes, Laravel is in the wrong here, but AIUI even a namespaced function would conflict with a soft-reserved keyword. And regardless, breaking Laravel is not a great plan.

1. Java uses a parenthetical block on `try` for similar functionality
(though without a separate context manager). That would look like:

try (new Foo() as $foo) {
  // ...
}
// catch and finally become optional if there is a context.

…

2. Either `use` or `using`. The semantics here would be identical to
the current `with` proposal.

IMO, the try syntax is more confusing than another overload of use.

cheers,
Derick

How about `using`?

--Larry Garfield

Hi Derick,

> On Wed, Nov 5, 2025, at 1:38 AM, Deleu wrote:
>
> > Out of curiosity, what happens if GOTO is used inside a context
> > block to jump away from it?
>
> That would be a success case, just like break or return. Basically
> anything other than an exception is a success case. (That said,
> please don't use Goto. :slight_smile: )

I do think you might need special attention to this case, as jumping out
of loops (such as foreach) needs to be handled with care.

In this case, goto is supported out of the box as anything necessary
to cleanup after with() is emitted in a finally block. So goto will
execute this before jumping to the actual target.

Best Regards,
Arnaud

Hi Paul,

On Wed, Nov 5, 2025 at 7:30 AM Paul Dragoonis <dragoonis@gmail.com> wrote:

3. In the implementation code there is a lot of mention of "list" and zend_list .. why? Maybe the answer is obvious but I can't see, at first glance, why we are implementing list under the hood.

`zend_list.h` is the API for managing resources in core.

Best Regards,
Arnaud

On Wed, Nov 12, 2025, 4:30 PM Arnaud Le Blanc <arnaud.lb@gmail.com> wrote:

Hi Paul,

On Wed, Nov 5, 2025 at 7:30 AM Paul Dragoonis <dragoonis@gmail.com> wrote:

  1. In the implementation code there is a lot of mention of “list” and zend_list .. why? Maybe the answer is obvious but I can’t see, at first glance, why we are implementing list under the hood.

zend_list.h is the API for managing resources in core.

Hi Arnaud,

Ok that answers it, thanks.

Best Regards,
Arnaud

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

I didn't mention this explicitly, so I'll do that now:

* We have changed the keyword from `with` to `using`, to avoid conflicting with existing global functions in Laravel.
* As requested, we've added support for multiple context managers in a single block. They simply transpile to nested try-catch-finally blocks.

There's still 2 outstanding questions:

* Is there any interest in turning `using` into an expression rather than a statement, so that it can be used in expression contexts?
* Does anyone want to argue about how `continue` should behave? Right now it matches `switch`, for better or worse. We're happy going with whatever the consensus is. If there's no feedback or consensus, we'll go with the current implementation.

To me, Rowan's experimentation really shows the value of splitting the manager variable from the context variable. The amount of flexibility gained is dramatic, and it makes many implementations much easier.

--Larry Garfield

On 2025-11-27 06:48, Larry Garfield wrote:

* Is there any interest in turning `using` into an expression rather than a statement, so that it can be used in expression contexts?

>
What would it evaluate to? The value returned by exitContext()? That's already controlling whether caught exceptions are rethrown, so the only value that would ever reach an enclosing expression would be "true".

(I've sometimes thought about making `return` an expression in the same spirit as `throw`. Its evaluated type would of course be "never" since any enclosing expression would be abandoned.)

Morgan

Hi

Am 2025-11-06 00:27, schrieb Rowan Tommins [IMSoP]:

I think you may have missed the key distinction between a "Context Manager" (as designed by Python) and a "Disposable" (as used in C# and others): the Context Manager is not the resource itself, it exists only to meet the protocol/interface.

In this code:

with ( $dbConnection->transaction() as $handle ) {
$handle->execute('I am in the transaction');
}

$handle is *not* the value returned by $dbConnection->transaction(), it's the value returned by $dbConnection->transaction()->enterContext().

For reference: This topic was/is also discussed in your sibling thread starting at php.internals: Re: Examples comparing Block Scoped RAII and Context Managers. I also stumbled upon this being completely unexpected from just looking at the code. If multiple participants on this list with a above-average knowledge of PHP (or even engine knowledge) find this unclear, I am really concerned about the clarity for “regular PHP developers”.

Best regards
Tim DĂźsterhus

Hi

Am 2025-11-06 13:18, schrieb Rowan Tommins [IMSoP]:

I think the relationship to Iterators is significant: if you put an iterator in a variable, it's perfectly possible to use it in two different foreach statements, or manually call the interface methods, and get very confusing results.

But most of the time, you don't take a reference to the iterator at all, and the same would be true of Context Managers:

foreach ( $foo->iterate() as $item ) { ... }
with ( $foo->guard() as $resource ) { ... }

The reference to the sibling thread probably fits even better here than in my previous reply. So I'm putting another reference: php.internals: Re: [RFC] Context Managers

there's nothing stopping you calling __construct or __destruct as normal methods, and causing all sorts of unintended behaviour.

That is *technically* true and there are indeed some “expert-level” use cases where calling these manually - except for `parent::` I guess - is required (e.g. lazy objects), but there's a very significant difference: Those are magic methods following the naming scheme of magic methods. The `__` prefix is a visual indicator that the methods are somehow special and probably not meant to be used directly.

The documentation at PHP: Magic Methods - Manual states (highlighting mine):

Magic methods are special methods which **override PHP's default's action** when certain actions are performed on an object.

All methods names **starting with __** are reserved by PHP.

This is not true for the context manager methods. From a developer's PoV these are normal methods on a class with nothing screaming “be careful”. The interface *could* serve as a invitation for folks to read the documentation on how the interface methods are meant to be used, but we all know how that works in practice with an IDE suggesting normal-looking methods that appear to do the right thing (until they don't).

Best regards
Tim DĂźsterhus

Hi

Am 2025-11-09 16:07, schrieb Larry Garfield:

Bob, you seem to be focused on the "manual call" case. That... is not a case that exists. I cannot think of any situation where a user creates a context manager and then calls enter/exit context themselves that isn't a "stop that, you're doing it wrong" situation.

See my sibling reply to Rowan (php.internals: Re: [RFC] Context Managers).

By the same token, you *can* implement Iterator and then call current() and next() and valid() yourself... but odds are you're doing something wrong, and it's very easy to screw things up if you call those methods in the wrong order or too many times or whatever. If you implement Iterator, you're supposed to use it with foreach(). Doing anything else with it is "well technically that maybe works, but please don't."

You are wrong here. Manually interacting with an Iterator is not just safe, it is also necessary for some use cases. The simplest example would be iterating two Iterators in lock-step (i.e. doing a `zip()` operation / array_combine()).

As Rowan noted, calling __construct() or __destruct() yourself is also possible, and can cause things to go sideways, and if they do then it's your own damned fault, don't do that.

The same is true here. Someone manually calling enterContext and then not calling exitContext() is... simply not a use case that matters, because there's no good reason to ever do so. The only effort the RFC needs to make in this regard is say "don't do that."

See sibling reply for an explanation how `__construct()` is different.

Best regards
Tim DĂźsterhus

On 01/12/2025 14:26, Tim DĂźsterhus wrote:

You are wrong here. Manually interacting with an Iterator is not just safe, it is also necessary for some use cases. The simplest example would be iterating two Iterators in lock-step (i.e. doing a `zip()` operation / array_combine()).

It is certainly *possible* to use the Iterator methods safely without a foreach() construct or built-in aggregate; but it's also easy to miss the implications, and cause very confusing behaviour. For instance, you could call next() in two different pieces of code, and each would miss half the items; or you could call rewind() on an iterator that was already in use elsewhere.

The same is true of the ContextManager interface: you could certainly call enterContext() and exitContext() manually with no problems at all. It might even be necessary, for much the same reasons as iterators: you might want to write code that aggregates two context managers in a specific arrangement.

As I understand it, your concern is that someone will call enterContext() without exitContext(). You could do exactly the same thing if you manually handle an Iterator.

Consider this method:

function getDataIterator(): Generator {
$this->acquireLock();
while ( $this->hasMoreResults() ) {
yield $this->getNextResult();
}
$this->releaseLock();
}

If the user calls $it = $foo->getDataIterator(), they can call $it->current() and get a result, but never call $it->next(). They could even use it in a foreach() loop but `break` out before consuming all the results. In such cases, the lock would never be released, unless there's an extra safety check in the destructor.

I think the currently proposed names actually make that *less* likely for a ContextManager: if you enter something, you probably want to exit it later.

We could reinforce that further by also prefixing the method names with "__", but we don't have any precedent for that, and misuse would still be possible.

--
Rowan Tommins
[IMSoP]

On Mon, Dec 1, 2025, at 9:38 AM, Rowan Tommins [IMSoP] wrote:

On 01/12/2025 14:26, Tim DĂźsterhus wrote:

You are wrong here. Manually interacting with an Iterator is not just
safe, it is also necessary for some use cases. The simplest example
would be iterating two Iterators in lock-step (i.e. doing a `zip()`
operation / array_combine()).

It is certainly *possible* to use the Iterator methods safely without a
foreach() construct or built-in aggregate; but it's also easy to miss
the implications, and cause very confusing behaviour. For instance, you
could call next() in two different pieces of code, and each would miss
half the items; or you could call rewind() on an iterator that was
already in use elsewhere.

The same is true of the ContextManager interface: you could certainly
call enterContext() and exitContext() manually with no problems at all.
It might even be necessary, for much the same reasons as iterators: you
might want to write code that aggregates two context managers in a
specific arrangement.

As I understand it, your concern is that someone will call
enterContext() without exitContext(). You could do exactly the same
thing if you manually handle an Iterator.

Consider this method:

function getDataIterator(): Generator {
$this->acquireLock();
while ( $this->hasMoreResults() ) {
yield $this->getNextResult();
}
$this->releaseLock();
}

If the user calls $it = $foo->getDataIterator(), they can call
$it->current() and get a result, but never call $it->next(). They could
even use it in a foreach() loop but `break` out before consuming all the
results. In such cases, the lock would never be released, unless there's
an extra safety check in the destructor.

Excellent example, thank you.

I think the currently proposed names actually make that *less* likely
for a ContextManager: if you enter something, you probably want to exit
it later.

We could reinforce that further by also prefixing the method names with
"__", but we don't have any precedent for that, and misuse would still
be possible.

Python uses magic methods here, and we considered it, but given that you will basically always want both methods we felt an interface was easier and provided better type checks. If there were multiple callbacks (say, a different one for a success exit and a fail exit), then magic methods might make more sense.

If you're suggesting having interface methods that begin with __, I agree that would be novel for no particular value. You can't screw up ContextManagers through misuse any more than you can screw up Iterator or ArrayAccess, and we've survived just fine with those for literally decades.

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

More updates to Context Managers:

* We have added "masking" for the context variable, using essentially the same technique as the block scope RFC.
* We have added support for `try using`, as a shorthand for when you want to wrap a try-catch-finally around a using statement anyway.

More details of both are in the RFC.

As no one seems to have a strong opinion on `continue`, we will most likely proceed with the current approach of matching `switch` behavior.

There doesn't seem to be much interest in making `using` an expression, which I find unfortunate, but that means we'll probably drop that. Fortunately it is probably possible to change in the future if the need arises (the way `throw` was changed).

--Larry Garfield

On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:

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:

More updates to Context Managers:

* We have added "masking" for the context variable, using essentially
the same technique as the block scope RFC.
* We have added support for `try using`, as a shorthand for when you
want to wrap a try-catch-finally around a using statement anyway.

More details of both are in the RFC.

As no one seems to have a strong opinion on `continue`, we will most
likely proceed with the current approach of matching `switch` behavior.

There doesn't seem to be much interest in making `using` an expression,
which I find unfortunate, but that means we'll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the way `throw` was changed).

--Larry Garfield

Since the only feedback on what to use for "as" was that => makes sense, we have changed the RFC to use => instead. So the new syntax is

using (new CM() => $cVar) {
  // Do stuff here.
}

--Larry Garfield

On Mon, Dec 15, 2025, 3:19 PM Larry Garfield <larry@garfieldtech.com> wrote:

On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:

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.

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:

More updates to Context Managers:

  • We have added “masking” for the context variable, using essentially
    the same technique as the block scope RFC.
  • We have added support for try using, as a shorthand for when you
    want to wrap a try-catch-finally around a using statement anyway.

More details of both are in the RFC.

As no one seems to have a strong opinion on continue, we will most
likely proceed with the current approach of matching switch behavior.

There doesn’t seem to be much interest in making using an expression,
which I find unfortunate, but that means we’ll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the way throw was changed).

–Larry Garfield

Since the only feedback on what to use for “as” was that => makes sense, we have changed the RFC to use => instead. So the new syntax is

using (new CM() => $cVar) {
// Do stuff here.
}

Going to be controversial here, but this is confusing, because it operates in the exact opposite of every other usage of => we have. With associative arrays, the left is assigned to the expression on the right; with arrow functions, the return value is the expression on the right; with match, the expression on the right is returned.

This is going to be easy to get wrong.

–
Matthew Weier O’Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him

On Dec 15, 2025, at 16:29, Matthew Weier O’Phinney mweierophinney@gmail.com wrote:

On Mon, Dec 15, 2025, 3:19 PM Larry Garfield <larry@garfieldtech.com> wrote:

On Thu, Dec 4, 2025, at 10:46 AM, Larry Garfield wrote:

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.

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:

More updates to Context Managers:

  • We have added “masking” for the context variable, using essentially
    the same technique as the block scope RFC.
  • We have added support for try using, as a shorthand for when you
    want to wrap a try-catch-finally around a using statement anyway.

More details of both are in the RFC.

As no one seems to have a strong opinion on continue, we will most
likely proceed with the current approach of matching switch behavior.

There doesn’t seem to be much interest in making using an expression,
which I find unfortunate, but that means we’ll probably drop that.
Fortunately it is probably possible to change in the future if the need
arises (the way throw was changed).

–Larry Garfield

Since the only feedback on what to use for “as” was that => makes sense, we have changed the RFC to use => instead. So the new syntax is

using (new CM() => $cVar) {
// Do stuff here.
}

Going to be controversial here, but this is confusing, because it operates in the exact opposite of every other usage of => we have. With associative arrays, the left is assigned to the expression on the right; with arrow functions, the return value is the expression on the right; with match, the expression on the right is returned.

This is going to be easy to get wrong.

I agree with Matthew.

I think it makes more sense to reverse them, like this:

using ($cVar => new CM()) {
// Do stuff here.
}

I think it’s still clear what this is doing, when reading it.

Cheers,
Ben