Hi
Am 2025-12-10 00:19, schrieb Rowan Tommins [IMSoP]:
As promised, I eventually got back to this e-mail properly, and have
updated the examples in response.
Great timing. After being on a company retreat last week and doing day
job stuff earlier this week, I wanted to get back to the RFCs today and
planned to send a reminder.
The block scoping RFC has been updated to `let()`.
Updated.
You missed db-transaction/application/1b-raii-with-scope-block.php.
Let me begin with some more obvious fixes I found in this new iteration:
- In db-transaction/application/1c-raii-with-scope-declaration.php I am
noticing that the comment mentions “extra indent” which is not a case
for the 1b version either, due to the “single statement” variant being
used.
- file-handle/application/3a-ioc-current-closure.php is missing a
closing parens and semicolon in line 20.
- file-handle/application/3b-ioc-auto-capture.php is missing the same.
- Both also applies to the unguarded version and the file-object
version.
- file-handle-unguarded/application/1b-raii-with-scope-block.php is
missing a closing parens in line 11.
- file-object/application/1a-raii-no-helpers.php is unsetting a
`$fileWrapper` variable, this should probably be `$fh`.
- file-object/application/2-context-manager.php has broken indentation.
FWIW: Using VS Code to view the examples with syntax highlighting worked
surprisingly well despite the new keywords. It allowed me to easily spot
these typos.
Less obvious issues with the locked-pdf example:
- locked-pdf/application/0-linear-code.php: In this case you are closing
the lock before writing into the same output file, which makes locking
useless. Probably a good case in point that the “naive” implementation
is insufficiently safe. The “save” arguably also belongs into the try
rather than the finally, since we probably don't want to save in case of
exception.
- Looking further I notice that the locking issue also exists for the
other implementations.
- I'd argue that the ->save() belongs into the caller, rather than the
RAII object or context manager, since actually saving a file is business
logic that should not be hidden away.
- If you would make the changes, this would effectively become
equivalent to the Transaction example or the file-object example just
with the extra `flock()` call and some extra business-specific logic.
I think this example should be adjusted to make use of “external
locking”, i.e. using a dedicated reusable single-purpose lock object
that is independent of the resource in question, so that it is
sufficiently dissimilar from the transaction example (though I guess it
would then be equivalent to the wikimedia-at-ease example). For
reference the RAII example would be just this:
<?php
class Lock {
private $lock;
public function __construct(string $file) {
$this->lock = fopen($file, 'r');
flock($this->lock, LOCK_EX);
}
public function __destruct() {
fclose($this->lock);
}
}
let ($lock = new Lock()) {
perform_operation_under_lock();
perform_operation_under_lock($lock); // or possibly this to
“prove” to the function that a lock is held.
}
From what I see, the locked-pdf example can be removed entirely, since
it does not bring anything new to the table. Did I miss something?
1.
For db-transaction/implementation/1-raii-object.php I'd like to note
that it is not necessary to proxy execute() through the transaction
object. It could also be used as a simple guard object which only
purpose is to be constructed and destructed.
That's true. The "wikimedia-at-ease" example illustrates that style,
because there aren't any methods we'd want there at all.
The drawback I see is that on a longer block, you have to come up with
a name for that unused variable, and make sure you don't accidentally
unset or overwrite it.
Apparently Java's workaround for that is to allow "unnamed variables",
which have the expected lifetime, but can't be accessed:
https://openjdk.org/jeps/456
Interesting, thank you for that insight.
Notably, this is *not* the same meaning for "_" as, say, C#, where "_ =
foo()" tells the compiler to discard the return value of foo():
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards
Though I agree that such an unnamed variable is more commonly used as a
discard in the programming languages I'm familiar with. For me a
variable name like `$lock` or similar would be sufficiently descriptive
to prevent the value from being overwritten. With the “variable backup”
from the block scoping RFC even declaring the variable with a new block
wouldn't cause issues, since the old value would be implicitly kept
alive and restored when the inner block ends.
In the case of a transaction, it feels logical for at least the
commit() and rollback() methods to be on the Transaction class, but
other methods would be a matter of style. I've removed execute() to
take the simplest route.
That makes sense to me.
2.
The RAII object in 'file-handle' serves no purpose. PHP will already
call `fclose()` when the resource itself goes out of scope, so this is
existing behavior with extra steps. The same is true for the
0-linear-code example in file-object. You don't need the `fclose()`
there.
This is true as long as nothing stores an additional reference to the
file handle. Having a separate "guard object" doesn't fully prevent
this - a reference to the guard object itself could leak - but does
make it less likely.
It also gives somewhere to customise the open and close behaviour, such
as adding locking, or converting false-on-error to an exception (more
on that below).
Yes. But since there was a specific example that didn't make use of this
additional capability, I called out that specific example.
To see what it looks like, I've added a "file-handle-unguarded"
scenario, which exposes the handle much more directly: it doesn't force
fclose(), and leaves the application to handle the failure of fopen()
Thank you. With regard to
file-handle-unguarded/application/1b-raii-with-scope-block.php, I am not
happy with either of the examples, since the style is bad in different
ways. Based on the “Example showing the combination of let and if():”
from the RFC and the generally accepted “happy path first” when having
an if with an else block, I would personally write it like this:
let ($fh = fopen('file.txt', 'w') if ($fh !== false) {
try {
foreach ($someThing as $value) {
fwrite($fh, serialize($value));
}
} catch (\Exception $e) {
log('Failed processing the file in some way.');
}
} else {
log('Failed to open file.');
}
Here the “else” block is behaving quite similarly to a “catch” block in
that it does the error handling.
The CM example benefits from being able to "break" out of the using{}
block; as far as I can see, the current RFC doesn't allow that from a
let{} block, is that correct?
This is correct. But I believe with my previous suggestion of writing
the example being able to break out of the block is not necessary.
Personally I find it pretty unintuitive that `break;` would target the
`using()` block for the context manager. It feels pretty arbitrary, why
is it possible to break out of `using()`, but not out of `if ()` or
`try` or `catch ()`. Currently my mental model is that `break;` is used
with control structures that “do multiple things” (though switch should
just not have fallthrough by default and then it also would need break).
If for some reason, you would like to break out of `let()`, there are
some options that rely on `let()` being designed to compose well with
existing functionality:
Using a do-while(false) loop. This is a pattern that is somewhat known
from C as a “restricted” form of goto.
<?php
class Foo
{
public function __construct()
{
echo __METHOD__, PHP_EOL;
}
public function __destruct()
{
echo __METHOD__, PHP_EOL;
}
}
let ($foo = new Foo()) do {
if (random_int(0, 1)) {
echo "Breaking out", PHP_EOL;
break;
} else {
echo "Not breaking out", PHP_EOL;
}
echo "Bottom", PHP_EOL;
} while (false);
echo "After", PHP_EOL;
And of course a regular goto also works.
The docblock in locked-pdf/application/2-context-manager.php is
incorrectly copy and pasted.
Well spotted. Fixed.
I'm not sure what you changed, but it's still referring to
“Transaction”. I'm also now noticing that the same is true for
locked-pdf/application/1a-raii-no-helpers.php.
This really demonstrates the value of the "try using() { ... }"
short-hand Larry & Arnaud have added to the CM RFC - we need the try to
be on the *outside* of the block to catch the new exception. I presume
a similar "try let() { ... }" could be added to yours?
Yes, but if this is desired I would prefer not implement this as an
explicit “try let” special case, but rather by allowing `try` to be
followed by any statement (which includes block statements). This would
then automatically compose with `let()`, just like `let()` composes with
`if()` and would improve predictability of the language overall. It is
not entirely trivial to implement, since the “dangling else” ambiguity
(https://en.wikipedia.org/wiki/Dangling_else) would then exist as a
“dangling catch” ambiguity, but it should be possible.
Best regards
Tim Düsterhus