On 18/11/2025 17:23, Larry Garfield wrote:
One thing I definitely do not like is the need for a `FileWrapper` class in the 
RAII file-handle example.  That seems like an unnecessary level of abstraction 
just to squeeze the `fclose()` value onto the file handle.  The fully-separated 
Context Manager seems a more flexible approach.


Yes, exploring how exactly that flexibility could be used was part of my motivation for the examples I picked.

The downside is that it is slightly harder to understand at first glance: someone reading "using (file_for_write('file.txt') as $fh)" might well assume that $fh is the value returned from "file_for_write('file.txt')", rather than the value returned from "file_for_write('file.txt')->enterContext()".

What made sense to me was comparing to an Iterator that only goes around once - in "foreach (files_to_write_to() as $fh)", the "files_to_write_to()" call doesn't return $fh either, "files_to_write_to()->current()" does.


I also noted that all of the examples wrap the context block (of whichever 
syntax) in a try-catch of its own.  I don't know if that's going to be a common 
pattern or not.  If so, might it suggest that the `using` block have its own 
built-in optional `catch` and `finally` for one-off additional handling?  That 
could point toward the Java approach of merging this functionality into `try`, 
but I am concerned about the implications of making both `catch` and `finally` 
effectively optional on `try` blocks.  I am open to discussion on this front.  
(Anyone know what the typical use cases are in Python?)


Looking at the parser, I realised that a "try" block with neither "catch" nor "finally" actually matches the grammar; it is only rejected by a specific check when compiling the AST to opcodes. Without that check, it would just compile to some unnecessary jump table entries.


I guess an alternative would be allowing any statement after the using() rather than always a block, as in Seifeddine and Tim's proposal, which allows you to stack like this:

using ($db->transactionScope()) try {
    // ...
}
catch ( SomeSpecificException $e ) {
    // ...
}

Or, the specific combination "try using( ... )" could be added to the parser. (At the moment, "try" must always be followed by "{".)


As I noted in one of the examples (file-handle/application/1b-raii-with-scope-block.php), there is a subtle difference in semantics between different nesting orders - with "try using()", you can catch exceptions thrown by enterContext() and exitContext(); with "using() try", you can catch exceptions before exitContext() sees them and cleans up.

It seems Java's try-with-resources is equivalent to "try using()":

>  In a try-with-resources statement, any catch or finally block is run after the resources declared have been closed.


Regarding `let`, I think there's promise in such a keyword to opt-in to "unset this at the end of this 
lexical block."  However, it's also off topic from everything else here, as I think it's very obvious 
now that the need to do more than just `unset()` is common.  Sneaking hidden "but if it also implements 
this magic interface then it gets a bonus almost-destructor" into it is non-obvious magic that I'd 
oppose.  I'd be open to a `let` RFC on its own later (which would likely also make sense in `foreach` and 
various other places), but it's not a solution to the "packaged setup/teardown" problem.


I completely agree. I think an opt-in for block scope would be useful in a number of places, and resource management is probably the wrong focus for designing it. For instance, it would give a clear opt-out for capture-by-default closures:

function foo() {
   // ... code setting lots of variables ...
   $callback = function() use (*) {
        let $definitelyNotCaptured=null;
        // ... code mixing captured and local variables ...
    }
}


Which is exactly the benefit of the separation of the Context Manager from the 
Context Variable.  The CM can be written to rely on `unset()` closing the 
object (risk 2), or to handle closing it itself (risk 1), as the developer 
determines.


Something the examples I picked don't really showcase is that a Context Manager doesn't need to be specialised to a particular task at all, it can generically implement one of these strategies.

The general pattern is this:

class GeneralPurposeCM implements ContextManager {
    public function __construct(private object $contextVar) {}
    public function enterContext(): object { return $this->contextVar; }
    public functoin exitContext(): void {}
}

- On its own, that makes "using(new GeneralPurposeCM(new Something) as $foo) { ... }" a very over-engineered version of "{ let $foo = new Something; ... }"

- To emulate C#, constrain to "IDisposable $contextVar", and call "$this->contextVar->Dispose()" in exitContext()

- To emulate Java, constrain to "AutoCloseable $contextVar" and call "$this->contextVar->close()" in exitContext()

- To throw a runtime error if the context variable still has references after the block, swap "$this->contextVar" for a WeakReference in beginContext(); then check for "$this->contextVarWeakRef->get() !== null" in exitContext()

- To have objects that "lock and unlock themselves", constrain to "Lockable $contextVar", then call "$this->contextVar->lock()" in beginContext() and "$this->contextVar->unlock()" in exitContext()


The only things you can't emulate are:

1) The extra syntax options provided by other languages, like C#'s "using Something foo = whatever();" or Go's "defer some_function(something);"

2) Compile-time guarantees that the Context Variable will not still have references after the block, like in Hack. I don't think that's a realistic goal for PHP.


Incidentally, while checking I had the right method name in the above, I noticed the Context Manager RFC has an example using "leaveContext" instead, presumably an editing error. :)


Regards,

--
Rowan Tommins
[IMSoP]

Reply via email to