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

Reply via email to