Hi, On Sun, Nov 9, 2025 at 9:08 PM Tim Düsterhus <[email protected]> wrote: > 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.
As I'm seeing it, a File object that was explicitly closed would throw an exception like "FileIsClosedError". It would indicate a lifetime bug that needs to be fixed, not something that should be handled by the program. This is reasonable, as closing is a clear intent that the resource should not be used anymore. This is not an exception that needs to be handled/checked. Under these intentions, leaving the resource open (for the reason it's still referenced) and allowing writes to it would be much worse. BTW, has the idea of removing close() methods on resources been tried successfully in other languages? > > 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). This diverges considerably from the features that the RFC claims to be designed after. Other languages with these features forcibly close the resource, while languages favoring RAII idioms make it very obvious when variables have non-local lifetimes. I feel that the RFC is taking a risk, and doesn't build on proven features, as it states. IMHO it is encouraging an idiom that comes with many pitfalls in PHP. The exception/backtrace issue demonstrated by Ed and Claude is not easily fixable with attributes or weak references, as the resource can be referenced indirectly: https://3v4l.org/lsicN Exceptions/backtraces are not the only way to capture a variable. It can happen in explicit ways: ``` using ($fd = fopen("file", "r")) { $buffer = new LineBuffered($fd); } // $fd not closed ``` Of course we can can do this instead, but it's easy to forget, so it's a pitfall: ``` using ($fd = fopen("file", "r"), $buffer = new LineBuffered($fd)) { } ``` And now, all precautions that one should take for resources (do not create cycles, do not extend lifetime) should also be taken for anything the resource is passed to. Here we capture $this by declaring a closure: ``` class CharsetConversion { function __construct(private mixed $fd, private string $from, private string $to) { if (function_exists("iconv")) { $this->convert = fn($input) => iconv($this->from, $this->to, $input); } else { $this->convert = fn($input) => mb_convert_encoding($input, $this->to, $this->from); } } function readLine() { return ($this->convert)(fgets($this->fd)); } } using ($fd = new File("file", "r"), $conversion = new CharsetConversion($fd, "iso-8859-1", "utf-8")) { } // $fd not closed ``` Async frameworks are likely to hold onto resources, by design: ``` using ($fd = ..) { await(processFile($fd)); } // $fd not closed ``` In this case a proper exitContext() / dispose() would likely request the framework to stop watching $fd. > 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. There are a few differences between memory and other resources: * Cycles can retain both memory and external resources, but the GC is governed only by memory-related metrics. So, cycles are usually cleared before they become a problem WRT memory usage, but not WRT other limits. I believe this is the reason why other languages chose to not rely on finalizers to release non-memory resources. * Memory is usually less scarce than other resources (files, db connections) * Releasing memory is usually less time-sensitive than other resources (locks, transactions) * Memory usage can be observed in an easier way * Resources can be released explicitly, but not memory > > 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. I would represent this as a LockedFile object, and make it an error to access the object if it has been closed/discarded, so it's not needed to check its state explicitly before every access. This is under the assumption that if I closed or unlocked the file, I don't intend it to be used anymore. If it's still accessed, letting the access go through seems worse than terminating the program. > >> 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? I'm not sure. I would expect an I/O polling mechanism to remove the FD as soon as it's closed. > > 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. I disagree. I gave one counter example above. > And as outlined above, > making a variable escape the local scope needs some deliberate action. I also disagree. It's true in C++ or Rust, as extending the lifetime of a local var would be undefined behavior or a compile time error, but not in PHP. Doing anything useful with a resource will likely involve passing it to other functions, which increases the chances of it happening. See also the examples above. Best Regards, Arnaud
