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]
Hi
Am 2025-11-10 11:04, schrieb Rowan Tommins [IMSoP]:
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.
I can see how this is a possible source of confusion. The motivation of
this RFC was to improve handling of lifetimes (both regular variables
and resource objects) and for that we looked into the listed references,
since we were familiar with them to varying degrees. Afterwards we
looked at how these languages differ from PHP and adapted the concepts
into something that we believe fits the existing semantics of PHP best.
From my experience, taking another programming language's feature as-is
and putting it into PHP is almost never the right choice.
I also wouldn't say that the RFC is drawing an explicit comparison to
the other languages. It just lists them as references / source of ideas,
but I can see how we could spell out more explicitly why we made the
changes compared to those other languages or that those are just a rough
inspiration.
Syntax-wise the solution ended up similarly to those three languages,
but when we were looking at the semantics, PHP's destructor semantics
are quite different than those of the other languages. That's why the
semantics differ from the three references and became “block scoping it
is” for RFC we are proposing, since that implicitly handles resource
objects.
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.
That's fair. I'll look into adding more explicit comparisons to the RFC
together with Seifeddine. My short answer here would be that JavaScript
already had explicit scoping by means of var and thus folks are
already used to needing to declare variables. Moving to let is just a
smaller incremental change. This is different from PHP where all
variables exist implicitly, which also means that semantics from other
languages need to be adapted.
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:
The closest comparison to “specific kind of block” might perhaps be
older versions of C which require all variables to be declared - and for
them to be declared at the top of the scope without any logic running
in-between.
Shadowing: At the point of declaration, any other variable with the
same name becomes inaccessible, but not over-written. Result: 1, 1, 2,
1Forbidden 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, 1Forbidden 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)
As mentioned before, many other languages already require explicit
variable declarations for everything. For those it's fairly clear that
variables do not exist before their declaration. With PHP variables
implicitly coming to life on the first usage, we don't believe that any
of those existing semantics are fitting a PHP developer's intuition and
therefore opted to require all “block scoped” variables to be declared
right at the start of the block to avoid the ambiguity of when the block
scoping actually starts for a given variable. The semantics of the block
are then equivalent to shadowing after we made the changes to restore
the original value afterwards.
Best regards
Tim Düsterhus
That's fair. I'll look into adding more explicit comparisons to the
RFC together with Seifeddine. My short answer here would be that
JavaScript already had explicit scoping by means ofvarand thus
folks are already used to needing to declare variables. Moving to
letis just a smaller incremental change. This is different from PHP
where all variables exist implicitly, which also means that semantics
from other languages need to be adapted.
Technically, JavaScript variables don't have to be declared either; the
difference is that they are global by default, and since function-scope
is generally more useful, people are indeed very familiar with "var".
PHP does have optional keywords that declare a variable with specific
scope, just much less frequently used: "global" and "static". Regarding
the discussion on shadowing vs hoisting:
- Both "global" and "static" can shadow local variables, and indeed each
other: https://3v4l.org/vPr2A - Since the shadowing lasts until the end of the function, a shadowed
local variable is never re-instated, and will be de-allocated if it was
the only reference: https://3v4l.org/VBVkX - Somewhat unusually, they do this as run-time statements, so you can
conditionally shadow variables: https://3v4l.org/fK9VJ
Whether these are good semantics to copy for a block-scoped variable,
I'm not sure; but they are existing PHP semantics for a very similar
situation.
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:The closest comparison to “specific kind of block” might perhaps be
older versions of C which require all variables to be declared - and
for them to be declared at the top of the scope without any logic
running in-between.
The big difference is the need for an extra level of indent (or, at
least, an extra pair of braces, which most people will probably assume
needs an extra level of indent). More often than not, there is an
existing block you want to scope to - a particular loop, or a
conditional branch, etc.
I do see the advantage of forcing them to the start, though. Languages
in the Pascal family might be another comparison to explore - variables
are all declared in a separate block at the top of a function. Off-hand,
I'm not sure if any allow an arbitrary nested block just to introduce
additional variables.
Regards,
--
Rowan Tommins
[IMSoP]
Hi
Am 2025-11-11 23:43, schrieb Rowan Tommins [IMSoP]:
That's fair. I'll look into adding more explicit comparisons to the
RFC together with Seifeddine. My short answer here would be that
JavaScript already had explicit scoping by means ofvarand thus
folks are already used to needing to declare variables. Moving to
letis just a smaller incremental change. This is different from PHP
where all variables exist implicitly, which also means that semantics
from other languages need to be adapted.Technically, JavaScript variables don't have to be declared either; the
difference is that they are global by default, and since function-scope
is generally more useful, people are indeed very familiar with "var".
You are correct, my remark was too simplified. To add to that: If you
use "use strict"; then the implicit fallback to global variables will
not happen and an error will be emitted instead:
"use strict";
function foo() {
a = 1;
}
foo();
results in: "ReferenceError: a is not defined".
PHP does have optional keywords that declare a variable with specific
scope, just much less frequently used: "global" and "static". Regarding
the discussion on shadowing vs hoisting:
- Both "global" and "static" can shadow local variables, and indeed
each other: https://3v4l.org/vPr2A- Since the shadowing lasts until the end of the function, a shadowed
local variable is never re-instated, and will be de-allocated if it was
the only reference: https://3v4l.org/VBVkX- Somewhat unusually, they do this as run-time statements, so you can
conditionally shadow variables: https://3v4l.org/fK9VJWhether these are good semantics to copy for a block-scoped variable,
I'm not sure; but they are existing PHP semantics for a very similar
situation.
Thank you. I just learned that global didn't need to be at the start
of a function - which might perhaps be an indication that folks will
already put the global at the top in practice, possibly because it is
confusing otherwise. As a trivia knowledge I also learned that global
supports variable variables: https://3v4l.org/oQLjP
With regard to the RFC, I've adjusted the implementation (and RFC text)
to make the following changes:
- Neither
global, norstaticmay be used from within theuse()
block. -
staticvariables defined before theuse()may not be used.
staticvariables defined after are okay, since there is no
ambiguity. -
globalvariables may be used. This is consistent withunset()
allowing to break the relationship with the global:
https://3v4l.org/j2tRa. From what I understand,global $foo;is
equivalent to$foo = &$GLOBALS['foo'];.
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:The closest comparison to “specific kind of block” might perhaps be
older versions of C which require all variables to be declared - and
for them to be declared at the top of the scope without any logic
running in-between.The big difference is the need for an extra level of indent (or, at
least, an extra pair of braces, which most people will probably assume
needs an extra level of indent). More often than not, there is an
existing block you want to scope to - a particular loop, or a
conditional branch, etc.
Please note that the use() construct does not necessarily require
braces. Thus both of the following would already work:
use ($foo) if ($bar) {
//
}
and
if ($bar) use ($foo) {
//
}
I do see the advantage of forcing them to the start, though. Languages
in the Pascal family might be another comparison to explore - variables
are all declared in a separate block at the top of a function.
Off-hand, I'm not sure if any allow an arbitrary nested block just to
introduce additional variables.
We'll look into more research in that direction.
Best regards
Tim Düsterhus
PS: I'll be on vacation Thursday - Sunday, so please be prepared for
some delay in replying :-)
Hi
I do see the advantage of forcing them to the start, though. Languages
in the Pascal family might be another comparison to explore - variables
are all declared in a separate block at the top of a function.
Off-hand, I'm not sure if any allow an arbitrary nested block just to
introduce additional variables.We'll look into more research in that direction.
From what I see in Pascal you would need to declare a “nested
procedure” that can then access the variables of the outer prodecure,
not dissimilar from a Closure with autocapturing.
We have now added a “Design Choices” section to the RFC explaining why
we opted for “declarations need to be at the start of the scope”:
https://wiki.php.net/rfc/optin_block_scoping#design_choices
While it does not directly compare with any language (except for
JavaScript), it mentions auto-vivification and scope-introspection
functionality, which as far as we are aware doesn't exist to this extend
in (similar) languages with block scoping and which is an important part
of the requirement.
It also points out how it will not require additional nesting in
practice. When there's already a block (particularly if()), one can
put the block statement directly into the construct without additional
braces.
Given the previous opinions of “please don't overload use() further”,
we also renamed the keyword to let() which fits the “block scoping”
semantics best, particularly when also considering the possible future
scope for foreach() scoping.
Best regards
Tim Düsterhus
We have now added a “Design Choices” section to the RFC explaining why
we opted for “declarations need to be at the start of the scope”:
Hi Tim,
Thanks for the updates.
I think the reason comparing to other languages is so important relates
to what Steve Klabnik calls "the Language Strangeness Budget":
https://steveklabnik.com/writing/the-language-strangeness-budget/
Anything that is going to surprise users coming from other languages
carries a cost which we need to justify. Generally, that means either a)
the expected way wouldn't work in PHP for some reason; or b) we think we
can do better by learning from the problems of other languages.
In basically every language derived from ALGOL and not from Pascal,
variable declarations take the form of a statement inside a block, so
doing something different definitely costs us some Strangeness Budget.
You've tried to make the case that PHP has unique challenges with
variable declarations, but I'm not convinced.
In particular, I don't think this statement is accurate:
Other languages with block scoping, particularly statically typed
languages, avoid this ambiguity by requiring all variables to be
explicitly declared
In any language with ALGOL-style block scoping, you can write the
equivalent of this code:
some_code_here(); // A
{
my_var = something(); // B
let my_var; // C
}
Every such language has to answer the same question: does "my_var" at
line B refer to the block-scoped variable declared below it at line C?
If the answer is "yes", then any code outside the block, at section A,
is irrelevant. Either the declaration is "hoisted" as though lines B and
C were in the opposite order; or an error is raised because the variable
is accessed (B) before declaration (C).
If the answer is "no", then line B does whatever it would have done if
line C didn't exist. If it's mandatory to declare "my_var" in some
surrounding scope, an error will be raised if it wasn't; if the variable
can instead be implicitly created in some surrounding scope, it will be.
The only way to avoid the ambiguity is to forbid all statements
between the start of the scope and a declaration - that is, raise an
error even if line B doesn't reference "my_var". Notably, if you keep
the ALGOL-style declarations, you can start with this rule, and then
relax it later, as happened with C99.
So if it's not because we can't implement ALGOL-style declarations, is
there something we think we can do better than them?
As far as I can see, any proposed statement of the form "let($foo) { ...
}" is directly equivalent to ALGOL-style "{ let $foo; ... }"
The unique innovation appears to be when using it with a single
statement rather than a block, such as in this example from the RFC:
let ($user = $repository->find(1)) if ($user !== null) { ... }
With ALGOL-style declarations, that requires an extra pair of braces:
{ let $user = $repository->find(1); if ($user !== null) { ... } }
Since an if statement can also take a single statement, you can also
write this:
if ( $repository !== null ) let ( $user = $repository->find(1) ) { ... }
Which is equivalent to this in ALGOL-style:
if ( $repository !== null ) { let $user = $repository->find(1); ... }
This reversibility would also be there with other block types:
"let($foo=something()) while($bar) { ... }" is subtly different from
"while($bar) let($foo=something() { ... }", and so on.
It's an interesting feature, but whether it's worth the cost in
"strangeness", I'm not sure.
The specific case of foreach-by-reference is a strong one, but as
mentioned before, I think it would be better served by a specific syntax
like "foreach ( $foo as let &$bar )", which avoids the repetition of
"let($bar) foreach ( $foo as &$bar )".
On the other hand, I note that the "process_file" example in the RFC
can't make use of the single-statement form: "let ( $lock =
$file->lock(LockType::Shared) ) try { ... }" would be legal, but
wouldn't release the lock until after the catch block.
In the example given, that's an exit point of the function anyway (when
the new Exception is thrown), so a function-scoped variable would be
cleaned up at the same time as a block-scoped one.
If there is no code after "// The file lock is released here ...", then
the ALGOL style saves a pair of braces:
try {
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
} catch ...
If there is, it looks very similar:
try {
{
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
}
more_code_here();
} catch ...
Over all, I'm still not sold on having a special new block for this,
rather than just using "let" for optional declarations, as in JavaScript.
--
Rowan Tommins
[IMSoP]
I think the reason comparing to other languages is so important
relates to what Steve Klabnik calls "the Language Strangeness
Budget": https:// steveklabnik.com/writing/the-language-strangeness-
budget/Anything that is going to surprise users coming from other
languages carries a cost which we need to justify. Generally, that
means either a) the expected way wouldn't work in PHP for some
reason; or b) we think we can do better by learning from the
problems of other languages.
Oooh! This is very cool!
This sounds a lot like "Jakob's Law of Internet User Experience,"
which states:
Users spend most of their time on other sites. This means that users
prefer your site to work the same way as all the other sites they
already know.[^1]
I've given a talk where I argue this principle carries over to
developer experience, as well. That is, developers prefer their tools
(and languages) to work the same way as other tools they already know.
Cheers,
Ben
[^1]: This is from the article "End of Web Design," published in 2000 by
Jakob Nielsen. https://www.nngroup.com/articles/end-of-web-design/
Hi
Am 2025-11-29 20:12, schrieb Rowan Tommins [IMSoP]:
I think the reason comparing to other languages is so important relates
to what Steve Klabnik calls "the Language Strangeness Budget":
https://steveklabnik.com/writing/the-language-strangeness-budget/Anything that is going to surprise users coming from other languages
carries a cost which we need to justify. Generally, that means either
a) the expected way wouldn't work in PHP for some reason; or b) we
think we can do better by learning from the problems of other
languages.In basically every language derived from ALGOL and not from Pascal,
variable declarations take the form of a statement inside a block, so
doing something different definitely costs us some Strangeness Budget.
I generally agree that it is important to not needlessly invent new
stuff folks need to learn and I'm trying hard in the design of my RFCs
and the discussion of other RFCs to figure out how to simplify things or
make them compose better. Syntax that is unfamiliar to users coming from
other languages is bad, but that shouldn't come at the expense of users
that are already familiar with PHP or the internal consistency of PHP.
I'd also argue that the proposed let() block syntax is intuitive to
understand when seeing it when familiar with block scoping, so it has
only a small impact on the strangeness cost.
FWIW: The proposed let() block is not too dissimilar from the let <var> = <expr> in <expr> syntax available in ML-style languages such as
OCaml or Haskell and also Nix.
You've tried to make the case that PHP has unique challenges with
variable declarations, but I'm not convinced.In particular, I don't think this statement is accurate:
Other languages with block scoping, particularly statically typed
languages, avoid this ambiguity by requiring all variables to be
explicitly declared[…]
Every such language has to answer the same question: does "my_var" at
line B refer to the block-scoped variable declared below it at line C?
That sentence you quoted was specifically in the context of the initial
paragraph of that section, contrasting PHP - where block scoping is
expected to be used comparatively sparingly - against languages where
variable declarations are a more “bread and butter” part of the
development process, because formally / explicitly declaring variables
is a necessity for one reason or another.
The only way to avoid the ambiguity is to forbid all statements
between the start of the scope and a declaration - that is, raise an
error even if line B doesn't reference "my_var". Notably, if you keep
the ALGOL-style declarations, you can start with this rule, and then
relax it later, as happened with C99.So if it's not because we can't implement ALGOL-style declarations, is
there something we think we can do better than them?
I feel that the C99 requirements and syntax would still have more
ambiguity compared to the proposed let() syntax in cases like this:
{
let $foo = bar($baz); // What is $baz referring to? Particularly
if it is a by-reference out parameter.
let $baz = 1;
}
because there is a much less direct / less rigid relationship between
the individual let statements, leaving room for interpretation of
“what is considered a statement”. As an example, is a goto jump label a
statement?
{
let $foo = 1;
label:
let $bar = $foo++;
goto label;
}
Forcing all the declarations into a single statement would resolve that
ambiguity, but I feel like that those restriction would feel arbitrary
and have a strangeness cost without any of associated benefits that the
let() block has.
As far as I can see, any proposed statement of the form "let($foo) {
... }" is directly equivalent to ALGOL-style "{ let $foo; ... }"
Yes, that is my understanding.
The unique innovation appears to be when using it with a single
statement rather than a block, such as in this example from the RFC:let ($user = $repository->find(1)) if ($user !== null) { ... }
With ALGOL-style declarations, that requires an extra pair of braces:
{ let $user = $repository->find(1); if ($user !== null) { ... } }
[…]
It's an interesting feature, but whether it's worth the cost in
"strangeness", I'm not sure.
Being able to declare variables with “if” lifetime that I can also check
is a big part of the benefits of the proposed syntax and something I'm
missing in other languages. C++ as a language in the “PHP syntax family”
added it in C++17 with the following syntax (taken from
https://en.cppreference.com/w/cpp/language/if.html):
if (char buf[10]; std::fgets(buf, 10, stdin))
if (std::lock_guard lock(mx); shared_flag)
Translated to PHP this would be:
if (let $user = $repository->find(1); $user !== null) { }
which would somewhat match the syntax of a for() loop with the
semicolon. But then the more composable
let ($user = $repository->find(1)) if ($user !== null) { }
would not be so different syntax-wise and would not require adding the
grammar to each and every control structure.
On the other hand, I note that the "process_file" example in the RFC
can't make use of the single-statement form: "let ( $lock =
$file->lock(LockType::Shared) ) try { ... }" would be legal, but
wouldn't release the lock until after the catch block.
As discussed in the sibling thread, allowing a single statement on try
should be possible (if necessary with a special case for let):
https://news-web.php.net/php.internals/129582
Best regards
Tim Düsterhus
That sentence you quoted was specifically in the context of the
initial paragraph of that section, contrasting PHP - where block
scoping is expected to be used comparatively sparingly - against
languages where variable declarations are a more “bread and butter”
part of the development process, because formally / explicitly
declaring variables is a necessity for one reason or another.
I don't think that changes anything I said in my previous reply: as soon
as you declare a variable half-way through a block, there is an
ambiguity about its range of visibility. Having more variable
declarations makes that more likely to come up, not less, so I'm not
sure why you think it "avoids" the problem.
There's also an assumption that if PHP added block scoping, it would
only rarely be used. We have no way to know, but I'm not sure that's
true. I can easily imagine code styles adding a rule that all local
variables be declared at an appropriate level. I can also imagine new
users coming from other languages - particularly JS - adding "let" out
of habit, even if seasoned PHP coders wouldn't.
I feel that the C99 requirements and syntax would still have more
ambiguity compared to the proposedlet()syntax in cases like this:{
let $foo = bar($baz); // What is $baz referring to?
Particularly if it is a by-reference out parameter.let $baz = 1;
}
Probably the simplest solution is to re-use our existing definition of
"constant expression". In fact, we already have variable declarations
using that rule:
function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains
invalid operations
}
As an example, is a goto jump label a statement?
{
let $foo = 1;
label:
let $bar = $foo++;
goto label;
}
PHP already limits where "goto" can jump to; I don't know how that's
implemented, but I don't think we need to get into philosophical
definitions to say "you can't jump into the middle of a declaration list".
Or, we could just bite the bullet and answer the "which way does it
resolve" question, as loads of other languages have already done.
Being able to declare variables with “if” lifetime that I can also
check is a big part of the benefits of the proposed syntax and
something I'm missing in other languages.if (let $user = $repository->find(1); $user !== null) { }
I find this more readable than the proposed version:
let ($user = $repository->find(1)) if ($user !== null) { }
Skimming down a piece of code, I can spot where code is being run
conditionally without reading the condition itself:
if ............ {
With the proposed syntax, that first glance is:
let ........... {
On closer inspection, it's actually:
let ..... if ..... {
Maybe it's also because I've dabbled in Perl, which has post-fix
conditions, so a very similar line would have a very different meaning:
my $foo=do_bar() if ($baz != 0);
is equivalent to:
my $foo;
if ($baz != 0) { $foo=do_bar(); }
Which is also a word order we can use in English, e.g. "hang the wet
clothes inside if it is raining".
In terms of making it less of a special case, some languages have a ","
operator which lets you glue any two expressions together and get the
right-hand result.
In Perl, you can write this:
my $a = 'outer', $b = 'whatever';
if ( my $a='inner', $b == 'whatever' ) {
say $a; // 'inner'
}
say $a; // 'outer'
This gives the desired scope for $a, but the if statement is still just
accepting a single expression.
JavaScript has the same operator, but apparently doesn't allow "let" in
an expression, so you can write:
if ( a="inner", b=="whatever" ) { }
but can't use it to declare a local version of "a".
I haven't thought through exactly how to apply that to PHP, but it might
give us an option for "both and": a concise and reusable syntax for the
if use case, and a separate syntax for cases like the closure example I
gave earlier: https://externals.io/message/129059#129075
--
Rowan Tommins
[IMSoP]
function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains
invalid operations
}
Though this example is legal since 8.3.
function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains invalid operations
}Though this example is legal since 8.3.
Oh, I must have had the wrong version selected on 3v4l when I tested it. Thanks for pointing that out.
I guess the point stands that we could use it, if we really wanted to avoid deciding how ambiguous scope worked.
Rowan Tommins
[IMSoP]
Being able to declare variables with “if” lifetime that I can also
check is a big part of the benefits of the proposed syntax and
something I'm missing in other languages.if (let $user = $repository->find(1); $user !== null) { }
I find this more readable than the proposed version:
let ($user = $repository->find(1)) if ($user !== null) { }
Skimming down a piece of code, I can spot where code is being run
conditionally without reading the condition itself:if ............ {
With the proposed syntax, that first glance is:
let ........... {
On closer inspection, it's actually:
let ..... if ..... {
Maybe it's also because I've dabbled in Perl, which has post-fix
conditions, so a very similar line would have a very different meaning:my $foo=do_bar() if ($baz != 0);
is equivalent to:
my $foo;
if ($baz != 0) { $foo=do_bar(); }Which is also a word order we can use in English, e.g. "hang the wet
clothes inside if it is raining".In terms of making it less of a special case, some languages have a ","
operator which lets you glue any two expressions together and get the
right-hand result.In Perl, you can write this:
my $a = 'outer', $b = 'whatever'; if ( my $a='inner', $b == 'whatever' ) { say $a; // 'inner' } say $a; // 'outer'This gives the desired scope for $a, but the if statement is still just
accepting a single expression.JavaScript has the same operator, but apparently doesn't allow "let" in
an expression, so you can write:if ( a="inner", b=="whatever" ) { }
but can't use it to declare a local version of "a".
I haven't thought through exactly how to apply that to PHP, but it might
give us an option for "both and": a concise and reusable syntax for the
if use case, and a separate syntax for cases like the closure example I
gave earlier: https://externals.io/message/129059#129075
The more I think on this, the more I think that the auto-unsetting behavior of a let would be useful only in combination with some other existing block, not as its own block.
if (let $x=stuff(); $x < 1) { ... }
for (let $i = 0; $i < 10; $i++) { ... }
foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)
And so on. (I'm not sure if it makes sense on a while? Possibly.) Exact syntax above is just spitballing.
But that would allow for the mask/unset logic for variables that have special meaning in existing block constructs, which is generally what you'd be interested in. I don't think there's a huge use case for unsetting arbitrary variables in arbitrary places. It would also be cleaner than the current pattern of if ($x = stuff() && $x < 1) {}, which always felt clunky and "leaks" $x.
If you need some thing more arbitrary and custom than cleaning up an existing block construct, then the additional setup of a Context Manager is fully justified, and more robust.
--Larry Garfield
Hello,
The more I think on this, the more I think that the auto-unsetting behavior of a
letwould be useful only in combination with some other existing block, not as its own block.if (let $x=stuff(); $x < 1) { ... }
for (let $i = 0; $i < 10; $i++) { ... }
foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)
And so on. (I'm not sure if it makes sense on a while? Possibly.) Exact syntax above is just spitballing.
I remain skeptical about adding block scoping to PHP at all, but if we
do pursue this, I have concerns about the approach. I have been
working on some tools I need and spent quite some time on php scoping
at large, and in detail, that's mainly the reason I am jumping in for
a change.
My primary concern with the suggestions of integrating let into
existing control structures (if (let $x=stuff(); ...), for (let $i =
0; ...)) is ambiguity. The same for sequence-like syntax proposed
earlier. PHP doesn't currently have block scoping, and introducing it
in a way that reuses or resembles existing syntax patterns will be
difficult to distinguish from current behavior. This could create
hidden bugs that could be hard to catch. Without talking about
changing let to "semi" reserved word. Not sure what that means tho'.
:)
The 'using' syntax proposed earlier in this thread, mapping maybe the
'with' behavior may also provide such an unambiguous definition.
If we add block scoping, it needs to be extremely explicit and
unambiguous. The RFC's current syntax does achieve this. Alternative
syntaxes like using (potentially mapping to with behavior) might also
provide that clarity, whereas let integrated into control structures
may not.
I am also wondering about the actual need of having a block only
scope, and potentially for some variables only (if that's part of the
behaviors as well). When a scope is needed but one does not want to
define a separate function, closure and co are cheap now. But I may
miss some obvious benefits that could benefit from such additions.
That would be a great addition to the RFC, actual use cases and
benefits over alternative existing ways to do it. Almost all other
languages provide block scoping, except bash (nice friend here ;), but
it does not mean we have to go that way. If we do, I emphasize again,
it needs to be extremely explicit.
PS: If we were back to php2/3, heh, even 4, I would propose to simply
introduce block scoping in full, that would have been a more standard
approach. But time flies, and we are at php 8 and it is tricky to
introduce this at this stage :)
In short, If we do proceed, explicitness must be non-negotiable :)
best,
Pierre
@pierrejoye | http://www.libgd.org
Hi
Am 2025-12-13 04:18, schrieb Pierre Joye:
My primary concern with the suggestions of integrating let into
existing control structures (if (let $x=stuff(); ...), for (let $i =
0; ...)) is ambiguity. The same for sequence-like syntax proposed
earlier. PHP doesn't currently have block scoping, and introducing it
in a way that reuses or resembles existing syntax patterns will be
difficult to distinguish from current behavior. This could create
Yes, this is a big part of the reason we've chosen the current syntax
with its dedicated construct that also forces all block-scoped variables
to be defined together at the start of their respective scope.
hidden bugs that could be hard to catch. Without talking about
changing let to "semi" reserved word. Not sure what that means tho'.
:)
let() being a semi-reserved keyword means that it will become
unavailable for use as a free-standing function, since let($foo = 1);
would be ambiguous otherwise, but it will remain available as a method
name, since the -> makes it clear that it must be a method call.
The 'using' syntax proposed earlier in this thread, mapping maybe the
'with' behavior may also provide such an unambiguous definition.
Sorry, I'm afraid I don't understand what you mean by “mapping the
'with' behavior”.
I am also wondering about the actual need of having a block only
scope, and potentially for some variables only (if that's part of the
behaviors as well). When a scope is needed but one does not want to
define a separate function, closure and co are cheap now. But I may
miss some obvious benefits that could benefit from such additions.
Perhaps the new “Example showing memory-efficient batch processing”
example (added yesterday) is making a good case? The alternatives there
would be unset() or a function/Closure which then come with the
boilerplate of passing along all variables that are required for the
scaling logic to the function scope. I find having the logic inline to
be pretty natural in that example.
That would be a great addition to the RFC, actual use cases and
benefits over alternative existing ways to do it. Almost all other
languages provide block scoping, except bash (nice friend here ;), but
it does not mean we have to go that way. If we do, I emphasize again,
it needs to be extremely explicit.
The “Examples” section is starting with examples that show the feature
in isolation to make the semantics clear, but the later examples are
intended to be representative of real-world use-cases where we would've
liked to have block scoping in the past. Do you wish to see more
explicit comparisons there? Perhaps you have another use case to add?
PS: If we were back to php2/3, heh, even 4, I would propose to simply
introduce block scoping in full, that would have been a more standard
approach. But time flies, and we are at php 8 and it is tricky to
introduce this at this stage :)
Yes, that is also what the RFC tried to say in the “Design Choices”
section (and also in my replies to Rowan). We are adding block scoping
to a language after the fact and thus different design considerations
apply compared to a language where block scoping is an integral part of
the language semantics.
Best regards
Tim Düsterhus
Hi
Am 2025-12-12 22:54, schrieb Larry Garfield:
The more I think on this, the more I think that the auto-unsetting
behavior of aletwould be useful only in combination with some
other existing block, not as its own block.
We very strongly disagree on this. Arbitrary block scoped variables
outside of control structures have proven their value in other
programming languages and the same use cases also apply to the use in
PHP.
if (let $x=stuff(); $x < 1) { ... }
for (let $i = 0; $i < 10; $i++) { ... }
foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)
And so on. (I'm not sure if it makes sense on a while? Possibly.)
Exact syntax above is just spitballing.
It does make sense on a while:
let ($row) while ($row = $statement->fetch()) {
// …
}
But that would allow for the mask/unset logic for variables that have
special meaning in existing block constructs, which is generally what
you'd be interested in. I don't think there's a huge use case for
unsetting arbitrary variables in arbitrary places. It would also be
cleaner than the current pattern of if ($x = stuff() && $x < 1) {},
which always felt clunky and "leaks" $x.If you need some thing more arbitrary and custom than cleaning up an
existing block construct, then the additional setup of a Context
Manager is fully justified, and more robust.
We have added a new example use-case “Example showing memory-efficient
batch processing” to the RFC that shows the value of stand-alone block
scoping for a case where the goal is the unsetting and freeing of memory
and not the side-effect of calling __destruct(). Somehow attempting to
merge the block declaration of $scaled into the foreach() probably
not going to be particularly readable. Limiting block scoping to control
structure initializers blocks some use-cases and does not provide a
meaningful (syntactical) value-add over the dedicated construct that
composes with the existing control structures.
Best regards
Tim Düsterhus
Hi
Am 2025-12-11 23:21, schrieb Rowan Tommins [IMSoP]:
That sentence you quoted was specifically in the context of the
initial paragraph of that section, contrasting PHP - where block
scoping is expected to be used comparatively sparingly - against
languages where variable declarations are a more “bread and butter”
part of the development process, because formally / explicitly
declaring variables is a necessity for one reason or another.I don't think that changes anything I said in my previous reply: as
soon as you declare a variable half-way through a block, there is an
ambiguity about its range of visibility. Having more variable
declarations makes that more likely to come up, not less, so I'm
not sure why you think it "avoids" the problem.
The difference I'm seeing is that for languages where variable
declarations (and block scoping) are a core part of the language, the
scoping rules are “moulding” (if that word makes sense here) how code in
that language is written and how folks reason about the code. This is
different for a language where block scoping is added after-the-fact and
remains an optional part of the language.
There's also an assumption that if PHP added block scoping, it would
only rarely be used. We have no way to know, but I'm not sure that's
true. I can easily imagine code styles adding a rule that all local
variables be declared at an appropriate level. I can also imagine new
users coming from other languages - particularly JS - adding "let" out
of habit, even if seasoned PHP coders wouldn't.
From my experience, a majority of functions in modern code bases are
reasonably short and single-purpose where intermediate variables are
meant to live for the remainder of the function scope. And of course
with additions such as the pipe operator, the number of temporaries will
likely also go down further. From my own PHP code, I would guess block
scoping to be useful for less than 10% of functions. For the ones where
it would be useful, it would be very useful, though, since those are the
functions that are on the more complex end of things.
I feel that the C99 requirements and syntax would still have more
ambiguity compared to the proposedlet()syntax in cases like this:{
let $foo = bar($baz); // What is $baz referring to?
Particularly if it is a by-reference out parameter.let $baz = 1;
}Probably the simplest solution is to re-use our existing definition of
"constant expression". In fact, we already have variable declarations
using that rule:function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains
invalid operations
}
Morgan already correctly noted that static supports arbitrary
expressions nowadays. I would like to add that supporting arbitrary
expressions within the initializer is also something we expect from
block scoping to avoid boilerplate, since most if we don't store a
dynamically computed value in a variable, we might as well use a
constant or hardcode the value.ö
As an example, is a goto jump label a statement?
{
let $foo = 1;
label:
let $bar = $foo++;
goto label;
}PHP already limits where "goto" can jump to; I don't know how that's
implemented, but I don't think we need to get into philosophical
definitions to say "you can't jump into the middle of a declaration
list".
Another, perhaps better, example that is not handled well by any
C-derived language that we are aware of is block scoping in combination
with switch():
switch ($var) {
let $tmp;
case "foo":
let $tmp2;
break;
case "bar":
case "baz":
let $tmp2;
let $tmp3;
break;
}
Which of the $tmps is placed at the “start of a block”? What is the
end of the block for each of them? Is it legal for $tmp2 to be
declared in two locations?
Or, we could just bite the bullet and answer the "which way does it
resolve" question, as loads of other languages have already done.
Other languages have other ecosystems and other user expectations. PHP
has extensive “scope introspection” functionality by means of
extract(), compact(), get_defined_vars() and variable variables.
Folks are used to being able to access arbitrary variables (it's just a
Warning, not an Error to access undefined variables) and there's also
constructs like isset() that can act on plain old local-scope
variables. Adding semantics like the “temporal dead zone” from
JavaScript that you suggested in the other thread would mean that we
would need to have entirely new semantics and interactions with various
existing language features that folks already know, adding to the
complexity of the language. The RFC, as currently proposed, avoids all
that by preserving all the existing semantics about “variable existence”
and just adding the “backup and restore old value” semantics that are
known from other languages and reasonably intuitive to understand even
when not intimately familiar with block scoping.
let ($user = $repository->find(1)) if ($user !== null) { }
Skimming down a piece of code, I can spot where code is being run
conditionally without reading the condition itself:
For me this works, because the let() is preparing me that “this code
is doing user processing” and the if() is just an “implementation
detail” / “means to an end” of that. By the block scoping semantics I
know that when I read the closing brace, the user processing is
finished. The function is a <h1>, the user processing is a <h2> and the
if() is a <h3> if that analogy makes sense. If I just want to get an
overview over the function, I only care about the <h2> headings.
Maybe it's also because I've dabbled in Perl, which has post-fix
conditions, so a very similar line would have a very different meaning:
I understand that some languages have postfix conditions, but being able
to place an if() after another control structure is not a new thing.
The same would apply to:
foreach ($users as $user) if ($user->isAdmin()) {
echo "User is admin";
}
which is already valid PHP.
In terms of making it less of a special case, some languages have a ","
operator which lets you glue any two expressions together and get the
right-hand result.In Perl, you can write this:
my $a = 'outer', $b = 'whatever'; if ( my $a='inner', $b == 'whatever' ) { say $a; // 'inner' } say $a; // 'outer'This gives the desired scope for $a, but the if statement is still just
accepting a single expression.
The comma would leave ambiguity in cases like if (let $repository = $container->getRepository(), $user = $repository->find(1)). Are both
$repository and $user block-scoped or only $repository of them?
Assignments are valid expressions in a condition. That's probably why
C++ uses the ; as a delimiter there.
JavaScript has the same operator, but apparently doesn't allow "let" in
an expression, so you can write:if ( a="inner", b=="whatever" ) { }
but can't use it to declare a local version of "a".
I haven't thought through exactly how to apply that to PHP, but it
might give us an option for "both and": a concise and reusable syntax
for the if use case, and a separate syntax for cases like the closure
example I gave earlier: https://externals.io/message/129059#129075
Adding “inline” support for other control structures certainly is
something that can be done as future scope. But we believe the “top of
the block” semantics are important for block scoping to work well in PHP
due to its unique semantics and 30y history.
Best regards
Tim Düsterhus
PS: With that both Seifeddine and I are going to be enjoying our
end-of-the-year vacations and are expected to be back on the list next
year.
Hi
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
To also pull it into the top-level:
- We adjusted the keyword from
use()tolet(). This has a small BC
impact that we explained in the corresponding section. - We refined the entire RFC text to better explain the (design) choices,
the focus, to add examples and other clarification. - Since the initial version - but previously announced - the construct
will now reset variables to their original value. - And with the latest implementation, there are two new OPcodes that
simplify the implementation greatly, but that extensions working with
OPcodes need to learn about.
The implementation should be up to date with the latest changes.
Best regards
Tim Düsterhus
Hi
The implementation should be up to date with the latest changes.
The implementation was up to date, but one example in the RFC
accidentally had an outdated error message (I've been made aware off-list).
I've fixed the error message and also noted that the error messages are
not final and not part of the actual RFC to allow some flexibility to
enable the best possible error messages as part of the final review of
the implementation.
Best regards
Tim Düsterhus
Hi
I just became aware of a mistake in the desugaring at the start of the
proposal section:
The value of $c was not correctly kept after breaking the reference
with unset($c). This was fixed by adding a $c = $c_original;. The
"Simple example" in the Examples section already correctly represented
the intent with the $b variable, where it still is "original b"
within the let() block.
I have also clarified in the desugaring that when the original values
are restored after the block that the restored values will become a
reference again if it originally was a reference. As far as I am aware
this is not representable purely with PHP code, so it's just noted in
the comment.
Best regards
Tim Düsterhus
Hi
to also put it into the correct thread: As just mentioned in the
“Examples comparing Block Scoped RAII and Context Managers” thread I
have (tried to) improve the explanation at the start of the “Proposal”
section:
No functional changes have been made. Particularly the desugaring
already represented the described semantics. They are now just spelled
out more explicitly in “English prose”.
And with that I'm really going to be off for vacation.
Best regards
Tim Düsterhus