Hi
Am 2025-11-04 13:31, schrieb Arnaud Le Blanc:
The proposal relies on destructors or automatic closing of resources,
but
this should not be relied on when timing matters. In general,
destructors
should be avoided IMHO [4][5]. They are useful in languages with
stack-allocated variables because timing and order can be guaranteed,
but
not in heap-allocated languages with automatic GC. PHP
resources/objects
are heap-allocated, and its GC mechanism behavior/semantics is similar
to
Java's due to cycles: resource/objects are not guaranteed to be
closed/disposed of immediately, and the order in which this happens is
undefined.
This is misrepresenting how PHP’s semantics around lifetimes work and
using that as a strawman argument to build something that does not fit
the existing semantics of PHP / the direction PHP is taking as of late.
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.
Here are some use-cases that Python's `with`, C#'s `using`, or Java's
`try-with` were designed to address, but are not addressed by this RFC:
// Commit the transaction as soon as the block is left, or roll it back
if
an exception is thrown:
with ($db->beginTransaction() as $transaction) {
$transaction->execute(...);
$transaction->execute(...);
}
If $transaction escapes, it's not committed at the end of the block.
Regardless, it's not possible to automatically rollback the transaction
in
case of exception.
This is easily solved by making the “commit” operation explicit and not
relying on exceptions for control flow. The suggested implicit commit is
dangerous, since it might accidentally commit the transaction when
undesired (e.g. when adding a guard clause with an early return). Here's
an example:
<?php
final class Transaction {
private bool $finalized = false;
public function __construct() {
echo "BEGIN", PHP_EOL;
}
public function commit() {
$this->finalized = true;
echo "COMMIT", PHP_EOL;
}
public function __destruct() {
if (!$this->finalized) {
echo "ROLLBACK", PHP_EOL;
}
}
}
use ($t = new Transaction()) {
$t->commit();
}
Nevertheless, this RFC acknowledges that use case as part of the “Future
Scope” section, as Seifeddine also mentioned in a previous reply to
Edmond: https://news-web.php.net/php.internals/129076
// Close file descriptor as soon as the block is left:
with (get_fd() as $fd) {
// ...
}
If $fd escapes, it's not closed at the end of the block. This may
affect
the program's behavior is various ways:
* The system's file descriptor limit may be reached before the GC
triggers
* If $fd was a socket, and the other side waits for closing, it may
hang
* If $fd has unflushed writes, readers will have an inconsistent view
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.
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'd like to note again that “The system's file descriptor limit may be
reached before the GC triggers” is misrepresenting how lifetimes work in
PHP. Unless the file descriptor somehow ends up as a part of a cycle, it
will reliably be closed exactly when nothing holds a reference to it -
i.e. when nothing is interesting in making use of the FD any longer.
Being able to let resource objects escape is a feature, since this
allows to reliably pass locks around without the resource suddenly
getting 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) -
and static analysis tools need to report every single method call as
“might possibly throw an Exception”.
Introducing a Disposable interface (similar to C#'s IDisposable) to
allow
objects to define custom, explicit cleanup logic that is automatically
called by use.
I'm in favor of introducing this immediately, for the reasons above,
and
[…]
I refer to Seifeddine's reply to Edmond:
https://news-web.php.net/php.internals/129076
Best regards
Tim Düsterhus