On Wed, Apr 15, 2026, at 16:52, Larry Garfield wrote:
> On Tue, Apr 14, 2026, at 9:27 AM, Rob Landers wrote:
> > On Tue, Apr 14, 2026, at 16:18, Rob Landers wrote:
> >> 
> >> 
> >> On Tue, Nov 4, 2025, at 21:13, 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. 
> >>> :-)
> >>> 
> >>> 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. :-)
> >>> 
> >>> -- 
> >>>   Larry Garfield
> >>>   [email protected]
> >>> 
> >> 
> >> Hi Larry/Arnaud,
> >> 
> >> This is a pretty exciting thread and fascinating proposal. That being 
> >> said, I have a couple of subtle questions that don't seem to be answered 
> >> in the (very long) thread or the RFC itself -- If I missed it, please let 
> >> me know:
> 
> >>  1.  What happens if a Fiber is suspended in the using block and never 
> >> resumed? When is the using block released to clean up the context?
> 
> Since it decomposes to a try-catch-finally, it will exit whenever the finally 
> block would have run if you'd just typed out try-catch-finally yourself.  
> Arnaud checked, and confirmed that when the fiber is destroyed the using 
> block will exit in a success case (ie, exitContext(null)).
> 
> >>  2. There's still no mention of how this should affect debugging, will we 
> >> see the "desugared" or "sugared" version? Is that even a concern for the 
> >> RFC?
> 
> Error messages would see the original code, so "error on line X" would be 
> based on the original `using` block.  That's the same as any other desugaring 
> we already do.  (PIpes, PFA, constructor promotion, etc.)  Debuggers will see 
> the materialized opcodes, again, the same other desugaring cases.
> 
> >>  3. I will say it is weird to have exitContext return an exception; but 
> >> what happens if an exception is thrown during exitContext? Why not just 
> >> have it return void and throw if you need to throw instead of having two 
> >> paths to the same thing?
> 
> There's a subtle but important difference here: An exception passed through 
> exitContext() is the original exception from lower in the call stack, and its 
> backtrace will be the original location of the error.  An exception thrown 
> from within exitContext() itself indicates a failure that the Context Manager 
> is responsible for, usually an error in the exitContext() logic itself.
> 
> Technically a Context Manager can wrap-and-rethrow the exception if it wants, 
> but then it is "claiming ownership" over it, just like in any other case of 
> wrap-and-rethrow.
> 
> Our expectation is that 90% of the time, "let the exception propagate up 
> unimpeded" is the desired behavior.  This approach makes "return $e" the 
> right thing to do almost-always, which is nice and simple to remember.
> 
> See the "return values and exception handling" section for a discussion of 
> this in more detail.  As I said in a previous reply, our constraints are 
> different than Python's so we end up with a different solution.  If you have 
> a suggestion for an alternate approach to the problem, we're happy to listen.
> 
> >>  4. Looking at the desugared form ... I'm a bit confused: if exitContext 
> >> is called during the finally path and returns an exception, it is just 
> >> swallowed? But if it is thrown, it won't be?
> 
> The finally path is only reached in case of a successful exit.  Therefore 
> there is no exception to pass in, and thus returning an exception is 
> meaningless.  If exitContext() throws a new exception of its own (which would 
> indicate an error in its own logic), that  will just bubble up past the 
> `using` block entirely, which is what we want.
> 
> >>  5. That being said, I don't think the RFC shares with us when we should 
> >> return an exception vs. throw an exception.
> 
> See the "return values and exception handling" section.  If something there 
> isn't clear, let me know and I will try to clarify further.
> 
> >> — Rob
> >
> > Maybe the desugared version should look more like this?
> >
> > } catch (\Throwable $e) {
> >     try {
> >         $__mgr->exitContext($e);
> >     } catch (\Throwable $cleanupException) {
> >         throw new ContextManagerException(
> >             $cleanupException->getMessage(),
> >             previous: $e
> >         );
> >     }
> >     throw $e;
> > }
> >
> 
> I'm not sure I see a reason to force any new exceptions to be only of the 
> ContextManagerException type.  If there's a TypeError inside exitContext() or 
> something, I'd expect that to be propagated as a TypeError.
> 
> --Larry Garfield

Thanks Larry, this clears things up for me. The example I had in mind is 
distinguishing between errors in a transaction:

class DatabaseTransaction implements ContextManager
{
    public function __construct(private PDO $connection) {}

    public function enterContext(): PDO
    {
        $this->connection->beginTransaction();
        return $this->connection;
    }

    public function exitContext(?\Throwable $e = null): ?\Throwable
    {
        if ($e) {
            $this->connection->rollback(); // PDO throws: server has gone away
        } else {
            $this->connection->commit();
        }
        return $e;
    }
}

// Application code:
using ($db->transaction() => $conn) {
    $conn->execute('INSERT INTO orders ...'); // throws ValidationException
}

The application will see the PDOException about the server going away, but the 
original ValidationException that caused the rollback attempt is lost entirely. 
My point with my suggestion wasn't to hide an exception behind a specific type, 
but to use exception chaining native to PHP to preserve both independent 
failures in a way that an application can understand what actually happened.

In other words, as a developer, all I'd see is a rollback failed due to a 
server disconnection. I'd be missing what caused the rollback in the first 
place.

I'd be fine if the desugared catch path simply attached the original exception 
as `$previous` on whatever escapes `exitContext()`, so the root cause isn't 
lost. That's a one-line change in the engine: just set the previous property at 
the bottom of the cleanup exception's chain before rethrowing.

— Rob

Reply via email to