Re: [RFC] Context Managers

From: Date: Sun, 09 Nov 2025 15:01:58 +0000
Subject: Re: [RFC] Context Managers
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to internals+get-129163@lists.php.net to get a copy of this message
On Sat, Nov 8, 2025, at 1:54 PM, Rowan Tommins [IMSoP] wrote: > On 05/11/2025 22:38, Bob Weinand wrote: >> 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(). > > > The Context Manager is *given knowledge of* the exception, but it's not > obliged to change its behaviour based on that knowledge. I don't think > that makes the interface opinionated, it makes it extremely flexible. > > It means you *can* write this, which is impossible in a destructor: > > function exitContext(?Throwable $exception) { >     if ( $exception === null ) { >         $this->commit(); >     } else { >         $this->rollback(); >     } > } > > > But you could also write any of these, which are exactly the same as > they would be in __destruct(): > > // Rollback unless explicitly committed > function exitContext(?Throwable $exception) { >     if ( ! $this->isCommitted ) { >         $this->rollback(); >     } > } > > // Expect explicit commit or rollback, but roll back as a safety net > function exitContext(?Throwable $exception) { >     if ( ! $this->isCommitted && ! $this->isRolledBack ) { >         $this->logger->warn('Transaction went out of scope without > explicit rollback, rolling back now.'); >         $this->rollback(); >     } > } > > // User can choose at any time which action will be taken on destruct / exit > function exitContext(?Throwable $exception) { >     if ( $this->shouldCommitOnExit ) { >         $this->commit(); >     } else { >         $this->rollback(); >     } > } > > > You could also combine different approaches, using the exception as an > extra signal only if the user hasn't chosen explicitly: > > function exitContext(?Throwable $exception) { >     if ($this->isCommitted || $this->isRolledBack) { >         return; >     } >     if ( $exception === null ) { >         $this->logger->debug('Implicit commit - consider calling > commit() for clearer code.'); >         $this->commit(); >     } else { >         $this->logger->debug('Implicit rollback - consider calling > rollback() for clearer code.'); >         $this->rollback(); >     } > } > > > -- > Rowan Tommins > [IMSoP] Though one point to note here, $this->isCommitted on the context MANAGER is not the same as isCommitted on the context VARIABLE. So in the above examples you would have to either return $this from enterContext() (which is fine), or save a reference to the context variable in the manager and then check $this->txn->isCommitted (which is also fine). Which you choose is mostly an implementation detail, and it's fine either way; I just want to emphasize that a lot of the flexibility of context managers comes from the separation of the context manager from the variable. A context manager is not just an auto-unsetter, though that is part of what it does. It is more properly a way to abstract out and package up setup/teardown lifecycle management, which can differentiate between a success or failure case. (At least as much as PHP itself is able to right now.) That has a wide variety of use cases, only a few of which a simple destructor could handle. --Larry Garfield

« previous php.internals (#129163) next »