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

Reply via email to