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]