Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been
working on: "use construct (Block Scoping)".
We wanted to raise a few initial points:
The RFC proposes the use keyword. What are your thoughts on a new using keyword instead, similar to C# or Hack?
How do you feel about the questions raised in the "Open Issues" section?
What are your general thoughts on the RFC?
Please find the following resources for your reference:
RFC: https://wiki.php.net/rfc/optin_block_scoping
POC:
https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
Thanks,
Seifeddine Gmati.
We wanted to raise a few initial points:
The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?
I’m undecided on this. However, using might be easier to understand since use could be confused with the existing keyword.
How do you feel about the questions raised in the "Open Issues" section?
I prefer Option B for restoring the value.
What are your general thoughts on the RFC?
In general, I support this RFC. I’d like to see examples from other programming languages, though. You mention C# and Hack above. Can you elaborate in the RFC on how they implement this functionality? What about other programming languages? I know some (Rust maybe?) are scoped to blocks by default.
Cheers,
Ben
Hi
Am 2025-11-04 03:08, schrieb Ben Ramsey:
In general, I support this RFC. I’d like to see examples from other
programming languages, though. You mention C# and Hack above. Can you
elaborate in the RFC on how they implement this functionality? What
about other programming languages? I know some (Rust maybe?) are scoped
to blocks by default.
Please have a look at Seifeddine's previous reply to Edmond
(https://news-web.php.net/php.internals/129076) and my reply to Arnaud
that I just sent (https://news-web.php.net/php.internals/129087).
There are two things to consider when comparing against other
programming languages. PHP's semantics do not exactly fit any of these,
which means that transferring some concept from another programming
language directly does not work.
- Scoping:
Many programming languages with explicit variable declarations are block
scoped. This includes Rust (which you correctly mentioned), but also C,
C++, Java, JavaScript (with let and const). Some of them allow shadowing
variables from the scope and some don't.
- Handling of Lifetimes:
As I mentioned in my reply to Arnaud, PHP is pretty unique in the list
of programming languages with automated memory management in that it
does not primarily use an “unpredictable” garbage collector for memory
management, but instead uses reference counting with reliable destructor
semantics (that are documented). This is different from e.g. Java where
the so-called “finalizers” run at an unpredictable point in time when
the GC feels like cleaning up an object. PHP's semantics are close to
those of C++ (where this kind of memory management is called RAII) or
Rust.
For this reason, PHP just needs some generic “syntactic sugar” for
unset() that is compatible with all existing functionality using
__destruct() for those RAII semantics.
Best regards
Tim Düsterhus
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been working on: "use construct (Block Scoping)".
We wanted to raise a few initial points:
The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?
I think the three existing meanings of use are enough. A new keyword would be better.
But, if a new keyword, why not scoped?
How do you feel about the questions raised in the "Open Issues" section?
B feels more intuitive.
Cheers
Nick
Hi
Am 2025-11-04 06:16, schrieb Nick:
I think the three existing meanings of
useare enough. A new keyword
would be better.
But, if a new keyword, why notscoped?
Thank for the keyword suggestion. Personally I could also imagine
let(), possibly combined with an in:
let ($x = 10, $y) in {
var_dump($x); // int(10)
var_dump($y); // `NULL`
$z = 20;
}
Best regards
Tim Düsterhus
What would happen to the variables introduced in the statements
block, rather than in use()? Will they still be available outside of the
block?
use () {
$number = 42;
}
var_dump($number); // what happens here?
Also, is an empty list of vars in use (as in the example above) allowed?
--
Best regards,
Bruce Weirdan mailto:
weirdan@gmail.com
Hi
Am 2025-11-04 07:08, schrieb Bruce Weirdan:
What would happen to the variables introduced in the statements
block, rather than inuse()? Will they still be available outside of
the
block?
Yes. The construct only affects the variables listed in the “declaration
list”. I have adjusted the “simple example” to introduce a new variable
$z that is initially defined in the block, but not listed in the
declaration list to make that clearer.
use () { $number = 42; } var_dump($number); // what happens here?Also, is an empty list of vars in
use(as in the example above)
allowed?
That is a syntax error (unexpected )).
Best regards
Tim Düsterhus
Hello all!
Thank you for the RFC, it has been missing for many years.
If I understand correctly, are you proposing to call unset at the
end of the block?
I see that the Future Scope section mentions Disposable.
But if your goal is to introduce behavior based on Disposable,
wouldn’t that conflict with the logic of the current RFC?
I see a clear pitfall here.
If you accept this RFC with the unset operation, you will later need
a new keyword for Disposal, because these are two entirely different
scenarios.
(Should I explain why?)
In this context, I also see a problem, as if the RFC is trying to
introduce two different features into the language:
-
Scope – a visibility area. It is the scope that has the
unsetlogic. - Using – a guaranteed call of a disposal function.
Because of this, logical issues are likely to arise. If the RFC’s goal
is unclear and it tries to cover both tasks, the solution risks losing
its clarity.
P.S.
Regarding the questions in the Open Issues, option A seems to have
more explicit behavior than option B.
Best Regards, Ed
Hello all!
Thank you for the RFC, it has been missing for many years.
If I understand correctly, are you proposing to callunsetat the
end of the block?I see that the Future Scope section mentions
Disposable.
But if your goal is to introduce behavior based onDisposable,
wouldn’t that conflict with the logic of the current RFC?
I see a clear pitfall here.If you accept this RFC with the
unsetoperation, you will later need
a new keyword forDisposal, because these are two entirely different
scenarios.
(Should I explain why?)In this context, I also see a problem, as if the RFC is trying to
introduce two different features into the language:
- Scope – a visibility area. It is the scope that has the
unsetlogic.- Using – a guaranteed call of a disposal function.
Because of this, logical issues are likely to arise. If the RFC’s goal
is unclear and it tries to cover both tasks, the solution risks losing
its clarity.P.S.
Regarding the questions in the Open Issues, option A seems to have
more explicit behavior than option B.
Best Regards, Ed
Hello,
Thank you for the feedback.
Regarding the Disposable interface: introducing it in the future
won't require new syntax. The use construct can work with both
__destruct (current behavior) and a future disposal interface.
The idea is to introduce an interface like:
interface Disposable {
public function dispose(?Throwable $throwable = null): void;
}
With use:
use ($foo = new Something()) {
// work
} // ->dispose(null) called on success, ->dispose($exception) on failure
This mimics Python's context manager protocol. The dispose() method
would be called before __destruct, allowing objects to distinguish
between successful completion and failure.
However, there's nothing stopping us from shipping without it. The
lock example in the RFC doesn't need this, locks are freed
automatically in __destruct. The initial version relying solely on
__destruct works fine for most use cases.
On the name "Disposable": I'm not really a fan of this name
myself. It was just my initial thinking when trying to copy Hack. We
can come up with something better later.
On Hack's approach: Hack has a Disposable interface, but it
works differently: disposable objects must maintain refcount=1, can't
be assigned to properties, and functions returning disposables need
<<__ReturnDisposable>>. This is enforced statically by their
analyzer. At runtime, they're just regular PHP objects. We can't
replicate this in PHP.
The refcount problem: A disposal interface has its issues. Once
dispose() is called, there may still be references to the object
somewhere, leaving it in an undesirable state, unless we add a way to
enforce refcount = 1.
A disposal interface without exception awareness brings limited value.
But with ?Throwable, it becomes useful:
use ($transaction = $ctx->beginTransaction()) {
// work
} // Transaction::dispose(?Throwable) called, then __destruct
Without dispose(?Throwable), the transaction can't know whether to
commit or rollback, __destruct alone can't distinguish success from
failure.
The key point: the initial version (relying on __destruct) and a
future disposal interface don't conflict.
Thanks,
Seifeddine Gmati.
Hello
A disposal interface has its issues.
Although Arnaud Le Blanc has already covered this topic thoroughly,
I’d like to approach it from a slightly different angle.
Let’s not think of enter/exit + RefCount as a problem. Every
approach has its own purpose. In this case, there is a clear
distinction between Scope logic and enter/exit logic. These
are two different concepts, and you cannot and should not try to
satisfy both RefCount and enter/exit conditions at the same
time.
The purpose of enter/exit is to handle the try-catch-finally
pattern for a resource regardless of the reference count. Therefore,
RefCount is not an issue.
(although PHP can automatically issue a warning when attempting to
call the method while the reference count is greater than one)
If PHP applies unset or enter/exit depending on whether an
interface is implemented, it will introduce hidden behavior in the
code, making it harder for developers to understand what is happening.
Compare the two cases:
// I know for sure that Scope implements the interface
// required to be used with "with"
with $scope = new Scope() {}
// I have no idea whether the File class implements
// the required interface or not. It’s unclear what will happen in the end.
with $file = new File("...") {}
So, in Python you cannot use arbitrary objects in a with statement,
only those that implement the enter and exit contract.
Therefore, in Python there is no ambiguity in the code. The developer
understands that if with is used, it means the object definitely
implements the required interface, otherwise an error will occur. PHP
must guarantee the same behavior.
P.S.
If I’m not mistaken, a recent RFC was proposed about context managers,
which covers exactly this logic.
Ed
If PHP applies unset or enter/exit depending on whether an
interface is implemented, it will introduce hidden behavior in the
code, making it harder for developers to understand what is happening.
Compare the two cases:// I know for sure that Scope implements the interface // required to be used with "with" with $scope = new Scope() {} // I have no idea whether the File class implements // the required interface or not. It’s unclear what will happen in the end. with $file = new File("...") {}So, in Python you cannot use arbitrary objects in a with statement,
only those that implement the enter and exit contract.
Hello Ed,
Thank you for your feedback. Regarding your concern about the clarity
when using a use statement with objects that may or may not
implement a Disposable interface, it does not matter to the
application developer whether a future Disposable interface is
implemented or not.
Consider this example:
// PHP Builtin:
interface DisposableInterface {
public function dispose(?Throwable $error): void;
}
// Library Code:
interface DatabaseTransaction extends DisposableInterface {
public function execute(string $q): void;
}
interface DatabaseConnection {
public function beingTransaction(): DatabaseTransaction;
}
interface DatabasePool {
public function getConnection(): DatabaseConnection;
}
// Application Code:
function do_work(DatabasePool $pool): void {
using (
$connection = $pool->getConnection(),
) {
using ($transaction = $connection->beingTransaction()) {
$transaction->execute('...');
sleep(10); // more work.
$transaction->execute('...');
}
sleep(10); // more work
}
sleep(10); // more work
}
In this scenario, the library author might not implement Disposable
for the DatabaseConnection because its internal handle is
automatically closed on __destruct, so to them, Disposable adds no
value. However, for the DatabaseTransaction, they do implement it,
as it allows the transaction to commit or rollback based on the exit
status.
From the application developer's perspective, both are temporary
resources that are "allocated" and will be disposed of after the
scope. How they are disposed of is decided by the maintainer of that
resource (in this example, a third-party library). They might feel
__destruct is sufficient (e.g., for a socket to be closed), or they
might feel the need for Disposable to perform a specific action
based on whether the operation finished successfully.
Thanks,
Seifeddine
Hello!
function addStudentLessons(DatabaseTransaction $transaction) {
try {
$transaction->execute(...);
} catch(\Exception $e) {
Logger::log($e);
throw $e;
}
}
// Application Code:
function do_work(DatabasePool $pool): void {
using (
$connection = $pool->getConnection(),
) {
using ($transaction = $connection->beingTransaction()) {
$transaction->execute('...');
sleep(10); // more work.
$transaction->execute('...');
}
sleep(10); // more work
}
sleep(10); // more work
} // <== broken!
Logger::log($e); <== reason!
In this example, the Logger service holds the exception $e,
which completely breaks the code because the transaction will no
longer complete correctly, and it’s unclear when the resources will be
released.
This is even more true for stateful applications, where the Logger
processes stored exceptions later rather than immediately.
Note that I didn’t even use circular references. I’m sure that 90% of
PHP developers who see this code won’t even understand what the
problem is.
And in practice, it will work for about 50% of them and fail for the other 50%.
At the same time, as a programmer, I didn’t do anything particularly
wrong or make any obvious mistake in this code. It’s just that the
logging service holds the exception object for a while.
RC-managed objects were designed to create code where the destruction
time of an object cannot be determined statically (only at runtime).
Automatic memory management is not a primary feature of RC objects,
since it can be implemented without reference counting.
However, the code in the example pursues the opposite goals: it must
guarantee the exact moment a function is called. In other words, the
RAII concept is not suitable here.
And this situation is typical for PHP stateful applications, where
resource control is not managed through RAII.
Best Regards, Ed
Le 6 nov. 2025 à 06:01, Edmond Dantes edmond.ht@gmail.com a écrit :
Hello!
function addStudentLessons(DatabaseTransaction $transaction) { try { $transaction->execute(...); } catch(\Exception $e) { Logger::log($e); throw $e; } } // Application Code: function do_work(DatabasePool $pool): void { using ( $connection = $pool->getConnection(), ) { using ($transaction = $connection->beingTransaction()) { $transaction->execute('...'); sleep(10); // more work. $transaction->execute('...'); } sleep(10); // more work } sleep(10); // more work } // <== broken!Logger::log($e); <== reason!
In this example, the
Loggerservice holds the exception$e,
which completely breaks the code because the transaction will no
longer complete correctly, and it’s unclear when the resources will be
released.
This is even more true for stateful applications, where the Logger
processes stored exceptions later rather than immediately.Note that I didn’t even use circular references. I’m sure that 90% of
PHP developers who see this code won’t even understand what the
problem is.
And in practice, it will work for about 50% of them and fail for the other 50%.At the same time, as a programmer, I didn’t do anything particularly
wrong or make any obvious mistake in this code. It’s just that the
logging service holds the exception object for a while.RC-managed objects were designed to create code where the destruction
time of an object cannot be determined statically (only at runtime).
Automatic memory management is not a primary feature of RC objects,
since it can be implemented without reference counting.However, the code in the example pursues the opposite goals: it must
guarantee the exact moment a function is called. In other words, the
RAII concept is not suitable here.
And this situation is typical for PHP stateful applications, where
resource control is not managed through RAII.
Best Regards, Ed
Hi,
Indeed, and here is a proof (variation on the “Example showing reliable resource management” of the RFC):
Something like a Disposable interface as suggested in the Future scope section, is probably the only way to make it reliable, and it ought to be part of the RFC.
—Claude
Hi
Indeed, and here is a proof (variation on the “Example showing reliable resource management” of the RFC):
Something like a
Disposableinterface as suggested in the Future scope section, is probably the only way to make it reliable, and it ought to be part of the RFC.
It is correct that Exceptions will capture parameters, but I don't agree
with the conclusion that this means that the “future scope” Disposable
interface is a necessity.
Exceptions will only capture the resource if it is passed to an external
function. In that case, the external function is already capable of
storing the resource somewhere else for future use. This means that the
code passing the resource elsewhere must already be prepared that it
might not close immediately.
In fact the callee might rely on being able to hold onto the resource
and it remaining valid. It would therefore be invalid for the caller to
forcibly close it - as I had also mentioned in my (first) reply to Arnaud.
The callee is also able to prevent the capturing of the lock in your
example, by moving it into a non-parameter local variable. Like this
(https://3v4l.org/tL5Yt):
$local_lock = $lock; unset($lock);
I'm not trying to say that this is obvious - I agree that Exceptions
capturing parameters is something that folks need to learn about - , but
it is a possible solution to this problem.
Given that I don't agree that Disposable is the solution to the
“Exception” issue, it's my responsibility to offer an alternative and I
would like to do this:
The addition of either an attribute or a marker interface for resource
objects that the backtrace capturing will observe - similarly to the
#[\SensitiveParameter] attribute. When an object is marked as a resource
object, then it will be wrapped into a WeakReference when the backtrace
is captured. As a result, the object will remain accessible for
backtrace processing / logging as long as it would still be alive if
there wasn't an Exception. But it will prevent the Exception from
unexpectedly extending the lifetime in other cases.
In addition I could imagine a WeakReference storing the class name of
the originally stored object as a “poor man's generic”, allowing folks
to learn which type of object was originally stored in the weak
reference, even when the original object is already gone.
Best regards
Tim Düsterhus
Hello.
I'm not trying to say that this is obvious - I agree that Exceptions
I only gave one of many examples that will inevitably occur if you
rely on reference counting.
My main point was something entirely different.
If the tool chosen to solve the problem is not well suited, then no
matter what methods are used to avoid mistakes,
it turns into an ongoing struggle that only adds complexity and
increases the chance of error.
Best regards
Ed
Hi
My main point was something entirely different.
If the tool chosen to solve the problem is not well suited, then no
matter what methods are used to avoid mistakes,
it turns into an ongoing struggle that only adds complexity and
increases the chance of error.
Virtually every (design) decision to make is a trade-off.
I only gave one of many examples that will inevitably occur if you
rely on reference counting.
And therefore making generic claims of “many problems” without
specifying them is not conductive to a discussion to figure out:
- If the stated problems are a problem in practice or if they are mostly
theoretical. - Possible solutions to the problems (such as the “WeakReference
capturing of arguments”). - If the described problem even exists or if it is based on a
misunderstanding of either the RFC or PHP's existing behavior.
Best regards
Tim Düsterhus
Hello.
If the stated problems are a problem in practice or if they are mostly theoretical.
Accidental retention of objects in unpredictable situations is a
constant issue for applications that do not terminate after each
request.
Possible solutions to the problems (such as the “WeakReference capturing of arguments”).
As a rule, any error can be fixed, but that is not an argument in this
situation. A developer can certainly solve a problem if they are aware
of it. The issue is that if you rely on references, they might never
even realize it exists :)
The problem is not fixing the error, but how easy it is to detect it.
If the described problem even exists or if it is based on a misunderstanding of either the RFC or PHP's existing behavior.
Wasn’t the example above convincing?
Best regards
Ed
This mimics Python's context manager protocol. The
dispose()method
would be called before__destruct, allowing objects to distinguish
between successful completion and failure.
A clarification here: this would be equivalent to IDisposable in C#, but it would not be equivalent to the Context Manager protocol in Python.
The use cases which motivated them are actually quite different: C# needed a way to handle things like pointers to unmanaged memory, so the design is closely tied to the actual resource object cleaning up its own internal state. Python was looking much more generally at common programming patterns, and a "context manager" can be separate from the resource it is managing, or even have no associated resource at all, only "enter" and "exit" behaviour.
Rowan Tommins
[IMSoP]
This mimics Python's context manager protocol. The
dispose()method
would be called before__destruct, allowing objects to distinguish
between successful completion and failure.A clarification here: this would be equivalent to IDisposable in C#, but it would not be equivalent to the Context Manager protocol in Python.
The use cases which motivated them are actually quite different: C# needed a way to handle things like pointers to unmanaged memory, so the design is closely tied to the actual resource object cleaning up its own internal state. Python was looking much more generally at common programming patterns, and a "context manager" can be separate from the resource it is managing, or even have no associated resource at all, only "enter" and "exit" behaviour.
Rowan Tommins
[IMSoP]
Hi Rowan,
My statement that it closely relates to Python, is in the sense that
if we were to add a Disposable interface, it would need to add
additional value beyond what __destruct already provides. In C#,
Disposable::Dispose ( just like in Hack ) receives no information on
whether the using scope exited successfully or due to an exception,
so having this in PHP adds no value that __destruct doesn't already
provide.
Hi Seifeddine, Tim,
I'm in favor of a feature similar to those listed in the RFC (Python’s
with [1], C#’s using [2], Hack's using [6]), but the proposal is not
equivalent to these. There are major differences that prevent it from
addressing the same use-cases.
First, the RFC proposes that variables are only unset() when leaving the
block, while Python, C#, Hack, and Java-s' try-with [3] (which is not
cited, but is similar), also immediately "close" or "dispose" of the
resource/object when leaving the block. This is important, as there are a
number of cases in which unset() alone will not immediately call a
destructor or close a resource.
Then, at least in Python, disposal is made aware of exceptions, so that it
can take different steps in that case.
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.
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.
// 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
// Await Scope at end of block:
with (new Async\Scope() as $scope) {
// ...
}
Again, if $scope escapes, it's not awaited at the end of the block, and
it's not possible to automatically cancel in case of exception.
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.
The RFC proposes the
usekeyword. What are your thoughts on a new
usingkeyword instead, similar to C# or Hack?
A possible alternative that doesn't introduce a new keyword is Java's try
() syntax.
How do you feel about the questions raised in the "Open Issues" section?
I would prefer Option B (Restore), as this is what I would expect from
block scoping.
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
also because introducing this later would make it difficult to adopt the
interface (implementing IDisposable on an existing class breaks existing
code using it in use()). I have a preference for Python's interface, as it
allows to optionally decouple (and hide) the dispose logic from the
resource, makes it possible to trigger a different behavior on exception,
and also makes it easier to introduce disposables without breaking code.
Also, making the interface optional, such that use($foo) is allowed when
$foo does not implement it, may mask programming mistakes and make the
feature confusing.
[1] https://peps.python.org/pep-0343/
[2]
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/using
[3]
https://docs.oracle.com/javase/8/docs/technotes/guides/language/try-with-resources.html
[4] https://externals.io/message/125696#125710
[5] https://openjdk.org/jeps/421
[6] https://docs.hhvm.com/hack/statements/using
Best Regards,
Arnaud
On Mon, Nov 3, 2025 at 10:47 PM Seifeddine Gmati azjezz@carthage.software
wrote:
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been
working on: "use construct (Block Scoping)".We wanted to raise a few initial points:
The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?How do you feel about the questions raised in the "Open Issues"
section?What are your general thoughts on the RFC?
Please find the following resources for your reference:
RFC: https://wiki.php.net/rfc/optin_block_scoping
POC:
https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scopeThanks,
Seifeddine Gmati.
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#'susing, or Java's
try-withwere 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
Hi,
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:
- 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 - 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 ofstd::shared_ptr()(C++) orRc(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
Hi
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:
- 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- 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 againThe 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 theuse()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
Hi,
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 callingfstat(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:
- 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- 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 againThe 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:
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 thememory_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
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.
C# / .net has an ObjectDisposedException for this purpose. The
documentation does indeed advise against catching it:
In most cases, this exception results from developer error. Instead of
handling the error in a |try|/|catch| block, you should correct the
error, typically by reinstantiating the object.
https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception
Note that using() does not prevent this. For example, in the
following, the using() block will implicitly call ms.Dispose(), but the
variable is still in scope.
MemoryStream ms= new MemoryStream(16);
using (ms)
{
ms.ReadByte();
}
ms.ReadByte(); // throws ObjectDisposedException
Similarly, additional references to the object can be made with
lifetimes which exceed the using() block:
MemoryStream ms_outer;
using (MemoryStream ms_inner = new MemoryStream(16))
{
ms_inner.ReadByte();
ms_outer = ms_inner;
}
ms_outer.ReadByte(); // throws ObjectDisposedException
In Hack, I believe both of these would be rejected by the compiler,
because any object implementing IDisposable is subject to strict usage
restrictions.
--
Rowan Tommins
[IMSoP]
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been working on: "use construct (Block Scoping)".
We wanted to raise a few initial points:
• The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?• How do you feel about the questions raised in the "Open Issues" section?
• What are your general thoughts on the RFC?
Please find the following resources for your reference:
• RFC: https://wiki.php.net/rfc/optin_block_scoping
• POC: https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
Thanks,
Seifeddine Gmati.
Hello,
One thing that isn't clear with this RFC is what happens to things redefined in the scope?
$a = 10;
use ($a = 5) {
var_dump($a);
}
var_dump($a); // unset or 10?
— Rob
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been working on: "use construct (Block Scoping)".
We wanted to raise a few initial points:
• The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?• How do you feel about the questions raised in the "Open Issues" section?
• What are your general thoughts on the RFC?
Please find the following resources for your reference:
• RFC: https://wiki.php.net/rfc/optin_block_scoping
• POC: https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
Thanks,
Seifeddine Gmati.
Hello,
One thing that isn't clear with this RFC is what happens to things redefined in the scope?
$a = 10;
use ($a = 5) {
var_dump($a);
}var_dump($a); // unset or 10?
— Rob
lol, I just saw "open issues" ... so, never mind. I like option B.
— Rob
Hi
Am 2025-11-03 22:46, schrieb Seifeddine Gmati:
How do you feel about the questions raised in the "Open Issues"
section?
Given the immediate and clear unanimous responses preferring option B
(restoring the original values), this is something we'll go with. I'll
look into updating the implementation later this week and we'll then
update the RFC based on the insights coming out of the implementation
(e.g. the exact semantics and possible edge cases).
With regard to the naming, which also got multiple replies in favor of
not giving use() yet another purpose, Seifeddine is currently running
analysis on a large number of composer packages to determine the impact
of various possible alternative names (let, scope, using).
Best regards
Tim Düsterhus
Hi
Am 2025-11-04 16:50, schrieb Tim Düsterhus:
Given the immediate and clear unanimous responses preferring option B
(restoring the original values), this is something we'll go with. I'll
look into updating the implementation later this week and we'll then
update the RFC based on the insights coming out of the implementation
(e.g. the exact semantics and possible edge cases).
I just updated the implementation in the branch already. The RFC text
will follow.
Best regards
Tim Düsterhus
Hi
Am 2025-11-05 17:27, schrieb Tim Düsterhus:
Given the immediate and clear unanimous responses preferring option B
(restoring the original values), this is something we'll go with. I'll
look into updating the implementation later this week and we'll then
update the RFC based on the insights coming out of the implementation
(e.g. the exact semantics and possible edge cases).I just updated the implementation in the branch already. The RFC text
will follow.
The RFC text has also been updated now to describe and showcase the
“Backup and Restore” logic that will result in the semantics expected
from full block scoping.
Best regards
Tim Düsterhus
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been working on: "use construct (Block Scoping)".
We wanted to raise a few initial points:
The RFC proposes the
usekeyword. What are your thoughts on a newusingkeyword instead, similar to C# or Hack?How do you feel about the questions raised in the "Open Issues" section?
What are your general thoughts on the RFC?
Please find the following resources for your reference:
RFC: https://wiki.php.net/rfc/optin_block_scoping
POC: https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
Thanks,
Seifeddine Gmati.
Hello internals,
Following up on the keyword discussion: I ran an analysis tool
across 507,529 PHP files from the top +14,000 Composer packages.
Results: https://github.com/azjezz/php-syntax-analyzer#results
Summary: let, using, scope, and block have zero or minimal
conflicts (0-1 occurrences). with has 111 conflicts across multiple
packages and should be avoided if we decide to use a new keyword.
Thanks,
Seifeddine Gmati.
Hello internals,
Tim and I would like to open the discussion on our new RFC that we've been
working on: "use construct (Block Scoping)".
Hi both,
I agree with Ed and with Arnaud: this feels like it's trying to squeeze two different features into one syntax and ends up with an awkward version of both.
For what Python calls "context managers", it offers very little: the programmer is still reliant on reference counting and cycle collection to actually clean up the resource, and objects can't directly interact with the context life cycle.
Python in particular has a very carefully designed solution, and the PEP is well worth reading: https://peps.python.org/pep-0343/ I think most of that could be directly ported to PHP.
For block scoping of "normal" variables it feels clunky to add an extra block, rather than declaring the variable with a keyword like "let" or "var". This is particularly obvious in the foreach example, where the variable has to be named twice on one line:
use ($value) foreach ($array as &$value) {
Languages with a keyword for declaring variable scope instead let you write the equivalent of this:
foreach ($array as let &$value) {
I have said before that an opt-in block scope would solve my main concern about automatically capturing variables in closures, because you could write this to make scope explicit:
$foo = fn() {
let $localVar;
something($localVar, $capturedVar);
something_else();
}
With this proposal, that would again be rather verbose: a mandatory extra set of braces, to put the scope inside the closure:
$foo = fn() {
let($localVar) {
something($localVar, $capturedVar);
something_else();
}
}
I think splitting the two use cases (context managers and scoped variables) would allow us to have much better solutions for both.
Rowan Tommins
[IMSoP]
Hi
Apologies for the late response. An unexpected high priority task came
up at work on Thursday and I wanted to make sure to provide a proper reply.
I agree with Ed and with Arnaud: this feels like it's trying to squeeze two different features into one syntax and ends up with an awkward version of both.
We don't think so. Our goal with this proposal is - as the title of the
RFC suggests - making block scoping / limiting lifetimes of variables
more convenient to use. The goal is to make it easier for developers and
static analysis tools to reason about the code, for example because
variables are less likely to implicitly change their type.
For this reason the RFC very intentionally relies on the existing
semantics of PHP and leaves anything more complicated to future scope,
as Seifeddine also mentioned in response to Edmond.
For block scoping of "normal" variables it feels clunky to add an extra block, rather than declaring the variable with a keyword like "let" or "var". This is particularly obvious in the foreach example, where the variable has to be named twice on one line:
use ($value) foreach ($array as &$value) {
Languages with a keyword for declaring variable scope instead let you write the equivalent of this:
foreach ($array as let &$value) {
Requiring to declare all block scoped variables at the start of the
block is an intentional decision to keep the scope easy to reason about.
Consider this example:
function foo() {
$a = 1;
var_dump($a);
{
var_dump($a);
let $a = 2;
var_dump($a);
}
var_dump($a);
}
What would you expect the output of each of the var_dump()s to be?
With regard to the foreach, I agree there is no ambiguity. I can imagine
a follow-up that desugars:
foreach ($array as let &$value);
to
use ($value) foreach ($array as &$value);
Or if you feel that this is important to have right away, I can look
into how complicated the implementation would be?
I think splitting the two use cases (context managers and scoped variables) would allow us to have much better solutions for both.
As mentioned above, this RFC explicitly is not a context manager
proposal. It's scoping - but it would make the existing lifetime
semantics easier accessible for what you call context managers.
Given the opinions disliking overloading the use() keyword, I have
discussed the keyword choice with Seifeddine. Personally I prefer
let() over all alternatives for this reason.
Best regards
Tim Düsterhus
We don't think so. Our goal with this proposal is - as the title of
the RFC suggests - making block scoping / limiting lifetimes of
variables more convenient to use. The goal is to make it easier for
developers and static analysis tools to reason about the code, for
example because variables are less likely to implicitly change their
type.
Perhaps part of the problem is the comparisons the RFC calls on. It
mentions features from three existing languages, none of which is about
block scoping; and doesn't mention the ways other languages do
indicate block scoping.
In C#, all local variables must be declared before use, and are scoped
to the current block; the "using" keyword doesn't change that. Instead,
it is for a very specific purpose: correctly "disposing" what .net calls
"unmanaged resources" - normally, raw memory pointers passed out from
some non-.net library or OS call. While objects can have "finalizers"
which work very like destructors, .net only guarantees that they will be
run "eventually", when the object is garbage collected. The IDisposable
interface, and the "using" keyword which makes use of it, exist to
release the unmanaged resources at a deterministic moment.
The Hack "using" statement is clearly inspired by C#, but takes it
further: there are no destructors or finalizers, so the only way to
perform automatic cleanup is implementing the IDisposable interface. The
compiler then enforces various restrictions on that class: it can only
be created as part of a "using" statement, and can only be used in such
a way that it will be unreferenced after disposal. Unlike C#, Hack's
local variables are normally function-scoped, so it is significant that
disposal happens at the end of a block; but it is neither intended or
usable as a general-purpose block scoping mechanism.
The Python "with" statement is completely different. In the words of the
PEP that specifies it, it is "to make it possible to factor out standard
uses of try/finally statements". Like PHP and Hack, Python's local
variables are function-scoped; but the with statement doesn't change
that - if a variable is initialised in the "with" statement, it is still
available in the rest of the function, just like the target of "as" in a
PHP "foreach". The "with" statement isn't actually concerned with the
lifetime of that variable, and in fact can be used without one at all;
its purpose is to call "enter" and "exit" callbacks around a block of code.
If what you're interested in is a general-purpose block scope, none of
these are relevant examples. The most relevant that comes to my mind is
JavaScript's "let", which is an opt-in block scope added to an exsting
dynamic language. A comparison of that approach to what you propose here
would be interesting.
Consider this example:
function foo() {
$a = 1;
var_dump($a);
{
var_dump($a);
let $a = 2;
var_dump($a);
}
var_dump($a);
}What would you expect the output of each of the
var_dump()s to be?
Yes, this is a problem that any language with block-scoping has to
tackle. Since there are many languages which have that feature, I'm sure
plenty has been written on the pros and cons of different approaches.
I'm not aware of any language that requires a specific kind of block
in order to introduce a new scope, but that doesn't mean it's a bad
idea. The approaches I am aware of are:
-
Shadowing: At the point of declaration, any other variable with the
same name becomes inaccessible, but not over-written. Result: 1, 1, 2, 1 -
Forbidden shadowing: At the point of declaration, if there is another
variable of the same name in scope, an error occurs. Result: 1, 1, Error
(let statement must not shadow $a from outer scope) -
Hoisting: The variable declaration can occur anywhere in the scope,
and affects the whole scope even lines above it. Used by JavaScript's
"var" keyword, and very confusing. Result: 1, 2, 2, 1 -
Forbidden hoisting: The variable declaration can occur anywhere in the
scope, but lines above that point are forbidden from accessing it. Used
by JavaScript's "let" keyword, with the dramatic name "Temporal Dead
Zone". Result: 1, Error (must not access block variable $a before
declaration)
--
Rowan Tommins
[IMSoP]