Hi,

On Wed, Nov 5, 2025 at 1:39 PM Tim Düsterhus <[email protected]> wrote:
> PHP has gradually been moving towards “making illegal states
> unrepresentable”. With the migration from resources to objects and the
> removal of the associated `_close()` functions, PHP developers and
> static analysis tools can rely on the fact that having a reference to
> the object means that the reference will always be valid. This is also
> something that Kamil mentioned as a good thing in the RFC discussion for
> the PDO::disconnect() method:
> https://news-web.php.net/php.internals/128742

I'm all for making illegal states unrepresentable, and I'm glad that
PHP goes in this direction.

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.

> 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.

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

> 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?

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.

> > Escaping/capturing is difficult to avoid, especially in large code
> > bases,
> > as it can not be checked with static analysis, typing, or avoided by
> > means
> > of API design. Sometimes it's even necessary, e.g. a file descriptor
> > may be
> > referenced by an I/O polling mechanism.
> 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.

> 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

> PHP’s main mechanism of managing lifetimes is reference counting and by
> that its semantics are much closer to those of languages that you call
> “stack allocated”. Specifically PHP's semantics around resources and
> objects match the semantics of `std::shared_ptr()` (C++) or `Rc` (Rust),
> which - like PHP - are languages that guarantee that destructors are
> predictably executed. Namely exactly when the reference count falls to
> zero.
>
> This is also documented and thus an explicit part of the semantics that
> PHP users rely on - and not just an implementation detail:
> https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.destructor.
> The file locking example using Seifeddine's PSL library from the RFC is
> a real-world use case that successfully relies on these semantics.
>
> It is true that the point in time when the reference count falls to zero
> is unpredictable in case of cycles, since this is dependent on the
> assistance of the cycle collector. Cycles however are a comparatively
> rare situation, particularly when dealing with a resource object. These
> situations are also easy to resolve using the same mechanism that one
> would use in C++ to deal with shared_ptr cycles, e.g. by including a
> WeakReference for one of the directions.

I don't think that `use()` is an upgrade, if it means that I have to
think about refcounts, track reference cycles, and carefully add
WeakReferences. This seems like too low level considerations to have
when programming in a high level language.

This is not better than the problems it tries to fix.

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.

I don't agree that it's easy to resolve or to avoid cycles. There are
no tools to discover or prevent them, and they don't show up in CI.
They can be introduced at any time, so a program employing `use()`
that works as expected today may break later due to an unrelated
change. And it doesn't always depend on the application's own code,
sometimes this happens due to a library.

But cycles are not the only issue: Variables can be captured,
accidentally or not (e.g. by a logger/tracer/cache/library/eventloop),
without the knowledge of the programmer, increasing their refcount and
extending their lifetime.

Best Regards,
Arnaud

Reply via email to