Re: [RFC] Context Managers
| From: | Larry Garfield | 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