Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:129315 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id B9D231A00BC for ; Wed, 19 Nov 2025 22:19:28 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1763590773; bh=mib4MXjpVaCm3IZBgMobRf75E0zvF706KYixpFCLu+M=; h=Date:Subject:To:References:From:In-Reply-To:From; b=Oku3jmL4eG7L0j6mINVTBb6wR4JaXrfselSySyYj4hbFO3hxl+VPIvqIvbubLl6tz uMC130VXJPBqkNa4FPfb13VbfzFO0VulE9umFEQIXVQkxmG8ALytocF/kx33VTknq5 lZUDl9kbdbv79ghbh8AHVYYoHEYJ9kEMRJ5yOBqqc2ut1NXVCTXtw6cbmXbe08y4I/ NbQl+4vDOGwCUUe95KswEfO547MHyGBsCK0sW19QwAxmXLvbWrB58BhfkmtZcHt+I6 R3BmOSIF0D64j/v6Zh0mm9FqJG8uuARk1of7oERbKHT5Bwpoa7WD5e1Od1avqOte5W y08Uw/j2GJuXw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id E94BD1801ED for ; Wed, 19 Nov 2025 22:19:31 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.9 required=5.0 tests=BAYES_20,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from fhigh-b5-smtp.messagingengine.com (fhigh-b5-smtp.messagingengine.com [202.12.124.156]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 19 Nov 2025 22:19:31 +0000 (UTC) Received: from phl-compute-05.internal (phl-compute-05.internal [10.202.2.45]) by mailfhigh.stl.internal (Postfix) with ESMTP id 9986F7A01CC for ; Wed, 19 Nov 2025 17:19:25 -0500 (EST) Received: from phl-mailfrontend-02 ([10.202.2.163]) by phl-compute-05.internal (MEProxy); Wed, 19 Nov 2025 17:19:25 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=rwec.co.uk; h=cc :content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:subject :subject:to:to; s=fm3; t=1763590765; x=1763677165; bh=f7uQ7XO4SN QJqB0v047dWmZ/rZ+W8rgsngOYW1iwFoY=; b=QTfvbl3FYVRJFSwQAudBB1uVSH EBNqdGHAmv7GKaSYp5ylpXttkqhI9Xp42MVT4/b6nMumiwgW5UNQCgCRCSAFiXpm ckXamIAhrcRWQESO7NQXbo5Ppe0RcDKteAUOdV5o2b9d60CjYT5I49yrRPaK7nqX oYR7piiY0xg3EEKk0dn6NiyqRfoa5uli/wpwQjEPwMPDs1rCYoj7qFol/Bj3zJiN SwNzr3UuJy1xdQC0LEvjjhetrGAyZX2BMmcjgKOYLRdFbaWJH5/O3+ydQrOLutFC N/5lmhN1+W47WSNuQ5N0nKGBfofLcVlOsNjDIk0B2ityFZqnGDLybDdyAazw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= 1763590765; x=1763677165; bh=f7uQ7XO4SNQJqB0v047dWmZ/rZ+W8rgsngO YW1iwFoY=; b=fYH9eYyauf6cto+/i7zcRXqWKxL2aGF+qV3c5rapsq+Qgp4dTB6 5mNbLHo8uF4dMY1jfwZsAWt0yD2ZK/GZrqFVCvutgOE5zV6O1qLdBi2XjTzjbQfQ VKUkTc4m1a/2CnXUvDsrvFsNA04yWpNqiEHiWTqH5dsGuCx4/c4HMlQJbp6R/BDd 1/k6G/DnINHV9smGpQIem8E9VvWLvNyMAAXjplhqNk/QZlmV43VKJ3siUg3sYdDt i7iMHEbCnolamNfwmCee7HYZ+B+1dehdcACjAyxWRH9Q7dxEwQPBX9K7LTEnM334 LAHKpKYZ9Cuqw69OXXvl5zJ1lp+zoTsKeUw== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggddvvdehfeekucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucenucfjughrpegtkfffgggfuffvfhfhjgesrgdtreertd dvjeenucfhrhhomhepfdftohifrghnucfvohhmmhhinhhsucglkffoufhorfgnfdcuoehi mhhsohhprdhphhhpsehrfigvtgdrtghordhukheqnecuggftrfgrthhtvghrnhepheetle eiiefgueduieeuieffvdevheduueefkeejuefgffeftdeitdegtedtleetnecuvehluhhs thgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepihhmshhophdrphhhph esrhifvggtrdgtohdruhhkpdhnsggprhgtphhtthhopedupdhmohguvgepshhmthhpohhu thdprhgtphhtthhopehinhhtvghrnhgrlhhssehlihhsthhsrdhphhhprdhnvght X-ME-Proxy: Feedback-ID: id5114917:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Wed, 19 Nov 2025 17:19:24 -0500 (EST) Content-Type: multipart/alternative; boundary="------------PpqcY7v5LK0WbzOA8P4HWra3" Message-ID: Date: Wed, 19 Nov 2025 22:19:23 +0000 Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PHP-DEV] Examples comparing Block Scoped RAII and Context Managers To: internals@lists.php.net References: <26a2f13c-f318-4d6c-9595-bfaaebcbabcb@rwec.co.uk> <432ca4ad-7bcc-43bd-8e05-3121839b4ff7@app.fastmail.com> Content-Language: en-GB In-Reply-To: <432ca4ad-7bcc-43bd-8e05-3121839b4ff7@app.fastmail.com> From: imsop.php@rwec.co.uk ("Rowan Tommins [IMSoP]") This is a multi-part message in MIME format. --------------PpqcY7v5LK0WbzOA8P4HWra3 Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit 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] --------------PpqcY7v5LK0WbzOA8P4HWra3 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 8bit
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]
--------------PpqcY7v5LK0WbzOA8P4HWra3--