Hi

On 11/5/25 17:17, Arnaud Le Blanc wrote:
But I don't think this is achievable or desirable for objects that
represent external resources like files or connection to servers,
which is what with() and similar mechanisms target. These resources
can become invalid or operations on them can fail for reasons that are
external to the program state. Removing close() methods will not
achieve the goal of ensuring that these resources are always valid.

That is correct, but I don't think that this is an argument in favor of increasing the number of these situations. Even for “unreliable” external resources, introspection functionality generally is effectively infallible (i.e. it only fails in situation where the entire system is in a bad state).

Following our Throwable policy (https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables) I can meaningfully handle a “DiskFullException” when attempting to write into a file. But I handling a “FileHandleBrokenError” is not meaningful, particularly when it's something like calling `fstat(2)` which is explicitly acting on a file descriptor you are already holding.

If $fd escapes and is nevertheless closed at the end of the block, this
may affect the program's behavior in various ways:
- Suddenly any operation on the file descriptor fails.

This will also happen due to external factors, for example if the disk
becomes full. Having a File object that can not be closed doesn't
ensure that operations on it will not throw.

See above.

Regarding `use()`, there are two alternatives, with different outcomes:

  1. use() doesn't forcibly close resources: If a resource escapes
despite the intent of the programmer, the program may appear to work
normally for a while until the leak causes it to fail
  2. use() forcibly closes resources: If a resource escapes despite the
intent of the programmer, the program may fail faster if it attempts
to use the resource again

The second alternative seems better to me:

  * If a mistake was made, the program will stop earlier and will not
successfully interact with a resource that was supposed to be closed
(which could have unwanted results)
  * Troubleshooting will be easier than chasing a resource leak

This is based on the assumption that “escapes” are always unintentional, which I do not believe is true (as mentioned in the next quoted section).

Managing lifetimes properly is already something that folks need to do. You mention “file descriptor leak”, but this is no different from a “memory leak” that causes the program to to exceed the `memory_limit`, because some large structure was accidentally still referenced somewhere.

The problem and solution is the same for both cases and my understanding is that there is already tooling to assist with verifying that e.g. a PHPUnit test does not leak.

Being able to let resource objects escape is a feature, since this
allows to reliably pass locks around without the resource suddenly
getting unlocked.

Would you utilize `use()` to lock a file in cases where the lock is
supposed to outlive the `use()` block?

Yes. The `use()` is there to make sure that I properly clean up after myself. If I pass my resource to another function, then I'm still responsible to clean up after myself - and the called function is responsible to clean up after itself.

As an example use case, consider a function that takes a lock as a proof that some resource is locked and then either processes it immediately or stores the resource (incl. the lock) for later processing and then unlock it when it is done with the processing. The `use()` construct in the caller then ensures that for the “immediate” use case the lock is not held for longer than necessary.

In any case, the developer is in full control. Passing the lock to the other function and creating a function that takes a lock as proof are intentional acts.

In simple cases, the resource object will be a regular local variable that will not escape. The PSL Lock example from the RFC is such an example, it was specifically designed to be held in the local scope and users of PSL are already successfully using that pattern. The PSL library is developed and maintained by Seifeddine and he specifically co-authored the RFC to improve the use cases that users (of PSL) are already successfully applying in practice.

Making objects invalid to detect bugs can also be a feature: We could
make a LockedFile object invalid once it's unlocked, therefore
preventing accidental access to the file while it's unlocked.

To make this same, the state of the lock object would need to be checked before every access, which I believe is impractical and error prone. If you forget this check, then the file might already be unlocked, since every function call could possibly have unlocked the file by calling `->unlock()`.

By tying the lock to the lifetime of an object it's easy to reason about and to review: If the object is alive, which is easily guaranteed by looking if the corresponding variable is in scope, the lock is locked.

This is true, but equally affects “not closing” and “forcibly closing”
the resource. In case of forcibly closing, your I/O polling mechanism
might suddenly see a dead file descriptor (or worse: a reassigned one) -

The reassigned case can not happen in PHP as we don't use raw file
descriptor numbers.

I was thinking about the following situation:

- A file object is created that internally stores FD=4.
- The file object is passed to your IO polling mechanism.
- The file object is forcibly closed, releasing FD=4.
- FD=4 still remains registered in the IO polling mechanism, since the IO polling mechanism is unaware that the file object was forcibly closed.
- A new file object is created that internally gets the reassigned FD=4.
- The IO polling mechanism works on the wrong FD until it realizes that the file object is dead.

Am I misunderstanding you?

and static analysis tools need to report every single method call as
“might possibly throw an Exception”.

This is the case even if we removed every possible way to close a file
descriptor

See the top of this email.

The fact we had to introduce a cycle collector, and that most projects
don't disable it, shows that cycles exist in practice. The fact that
they exist or can be introduced is enough that thinking of PHP's GC
mechanism as something closer to a tracing GC is easier and safer, in
general. A resource doesn't have to be part of a cycle, it only needs
to be referenced by one.

The data structures that tend to end up circular, are not the data structures that tend to store resource objects. And as outlined above, making a variable escape the local scope needs some deliberate action. I expect it to be something done by more experienced PHP developers, which I'd claim are also the group of developers that carefully rely on the semantics of the language to keep their code safe.

Best regards
Tim Düsterhus

Reply via email to