[RFC][Discussion] use construct (Block Scoping)

php.internals

Seifeddine Gmati

301 days ago
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.

Ben Ramsey

301 days ago
> 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? >
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

Tim Düsterhus

300 days ago
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. 1. 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. 2. 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

Nick

301 days ago
> On 4. Nov 2025, at 04:46, 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 `use` keyword. What are your thoughts on a new `using` keyword 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

Tim Düsterhus

301 days ago
Hi Am 2025-11-04 06:16, schrieb Nick:
> I think the three existing meanings of `use` are enough. A new keyword > would be better. > But, if a new keyword, why not `scoped`?
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

Weirdan

301 days ago
What would happen to the variables introduced in the statements block, rather than in `use()`? Will they still be available outside of the block? ```php 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

Tim Düsterhus

301 days ago
Hi Am 2025-11-04 07:08, schrieb Bruce Weirdan:
> What would happen to the variables introduced in the statements > block, rather than in `use()`? 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.
> ```php > 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

Edmond Dantes

301 days ago
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: 1. **Scope** – a visibility area. It is the scope that has the `unset` logic. 2. **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

Seifeddine Gmati

300 days ago
On Tue, 4 Nov 2025 at 10:28, Edmond Dantes <edmond.ht@gmail.com> wrote:
> > 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: > > 1. **Scope** – a visibility area. It is the scope that has the `unset` logic. > 2. **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: ```php interface Disposable { public function dispose(?Throwable $throwable = null): void; } ``` With `use`: ```php 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: ```php 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.

Edmond Dantes

300 days ago
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: ```php // 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

Seifeddine Gmati

299 days ago
On Wed, 5 Nov 2025 at 06:27, Edmond Dantes <edmond.ht@gmail.com> wrote:
> > 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: > > ```php > // 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 // 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

Edmond Dantes

299 days ago
Hello! ```php 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

Claude Pache

297 days ago
> Le 6 nov. 2025 à 06:01, Edmond Dantes <edmond.ht@gmail.com> a écrit : > > Hello! > > ```php > 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
Hi, Indeed, and here is a proof (variation on the “Example showing reliable resource management” of the RFC): https://3v4l.org/KJaL1 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

Tim Düsterhus

296 days ago
Hi On 11/7/25 20:36, Claude Pache wrote:
> Indeed, and here is a proof (variation on the “Example showing reliable resource management” of the RFC): > > https://3v4l.org/KJaL1 > > 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.
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

Edmond Dantes

296 days ago
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

Tim Düsterhus

296 days ago
Hi On 11/9/25 18:54, Edmond Dantes wrote:
> 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

Rowan Tommins [IMSoP]

299 days ago
On 4 November 2025 19:44:40 GMT, Seifeddine Gmati <azjezz@carthage.software> wrote:
>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]

Seifeddine Gmati

299 days ago
On Wed, 5 Nov 2025 at 21:18, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
> > On 4 November 2025 19:44:40 GMT, Seifeddine Gmati <azjezz@carthage.software> wrote: > > >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.

Arnaud.lb

301 days ago
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 `use` keyword. What are your thoughts on a new
`using` keyword 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:

Tim Düsterhus

300 days ago
Hi Am 2025-11-04 13:31, schrieb Arnaud Le Blanc:
> The proposal relies on destructors or automatic closing of resources, > but > this should not be relied on when timing matters. In general, > destructors > should be avoided IMHO [4][5]. They are useful in languages with > stack-allocated variables because timing and order can be guaranteed, > but > not in heap-allocated languages with automatic GC. PHP > resources/objects > are heap-allocated, and its GC mechanism behavior/semantics is similar > to > Java's due to cycles: resource/objects are not guaranteed to be > closed/disposed of immediately, and the order in which this happens is > undefined.
This is misrepresenting how PHP’s semantics around lifetimes work and using that as a strawman argument to build something that does not fit the existing semantics of PHP / the direction PHP is taking as of late. PHP’s main mechanism of managing lifetimes is reference counting and by that its semantics are much closer to those of languages that you call “stack allocated”. Specifically PHP's semantics around resources and objects match the semantics of `std::shared_ptr()` (C++) or `Rc` (Rust), which - like PHP - are languages that guarantee that destructors are predictably executed. Namely exactly when the reference count falls to zero. This is also documented and thus an explicit part of the semantics that PHP users rely on - and not just an implementation detail: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.destructor. The file locking example using Seifeddine's PSL library from the RFC is a real-world use case that successfully relies on these semantics. It is true that the point in time when the reference count falls to zero is unpredictable in case of cycles, since this is dependent on the assistance of the cycle collector. Cycles however are a comparatively rare situation, particularly when dealing with a resource object. These situations are also easy to resolve using the same mechanism that one would use in C++ to deal with shared_ptr cycles, e.g. by including a WeakReference for one of the directions.
> Here are some use-cases that Python's `with`, C#'s `using`, or Java's > `try-with` were designed to address, but are not addressed by this RFC: > > // Commit the transaction as soon as the block is left, or roll it back > if > an exception is thrown: > with ($db->beginTransaction() as $transaction) { > $transaction->execute(...); > $transaction->execute(...); > } > > If $transaction escapes, it's not committed at the end of the block. > Regardless, it's not possible to automatically rollback the transaction > in > case of exception.
This is easily solved by making the “commit” operation explicit and not relying on exceptions for control flow. The suggested implicit commit is dangerous, since it might accidentally commit the transaction when undesired (e.g. when adding a guard clause with an early return). Here's an example: <?php final class Transaction { private bool $finalized = false; public function __construct() { echo "BEGIN", PHP_EOL; } public function commit() { $this->finalized = true; echo "COMMIT", PHP_EOL; } public function __destruct() { if (!$this->finalized) { echo "ROLLBACK", PHP_EOL; } } } use ($t = new Transaction()) { $t->commit(); } Nevertheless, this RFC acknowledges that use case as part of the “Future Scope” section, as Seifeddine also mentioned in a previous reply to Edmond: https://news-web.php.net/php.internals/129076
> // Close file descriptor as soon as the block is left: > with (get_fd() as $fd) { > // ... > } > > If $fd escapes, it's not closed at the end of the block. This may > affect > the program's behavior is various ways: > * The system's file descriptor limit may be reached before the GC > triggers > * If $fd was a socket, and the other side waits for closing, it may > hang > * If $fd has unflushed writes, readers will have an inconsistent view
If $fd escapes and is nevertheless closed at the end of the block, this may affect the program's behavior in various ways: - Suddenly any operation on the file descriptor fails. PHP has gradually been moving towards “making illegal states unrepresentable”. With the migration from resources to objects and the removal of the associated `_close()` functions, PHP developers and static analysis tools can rely on the fact that having a reference to the object means that the reference will always be valid. This is also something that Kamil mentioned as a good thing in the RFC discussion for the PDO::disconnect() method: https://news-web.php.net/php.internals/128742 I'd like to note again that “The system's file descriptor limit may be reached before the GC triggers” is misrepresenting how lifetimes work in PHP. Unless the file descriptor somehow ends up as a part of a cycle, it will reliably be closed exactly when nothing holds a reference to it - i.e. when nothing is interesting in making use of the FD any longer. Being able to let resource objects escape is a feature, since this allows to reliably pass locks around without the resource suddenly getting unlocked.
> Escaping/capturing is difficult to avoid, especially in large code > bases, > as it can not be checked with static analysis, typing, or avoided by > means > of API design. Sometimes it's even necessary, e.g. a file descriptor > may be > referenced by an I/O polling mechanism.
This is true, but equally affects “not closing” and “forcibly closing” the resource. In case of forcibly closing, your I/O polling mechanism might suddenly see a dead file descriptor (or worse: a reassigned one) - and static analysis tools need to report every single method call as “might possibly throw an Exception”.
>> Introducing a Disposable interface (similar to C#'s IDisposable) to >> allow > objects to define custom, explicit cleanup logic that is automatically > called by use. > > I'm in favor of introducing this immediately, for the reasons above, > and > […]
I refer to Seifeddine's reply to Edmond: https://news-web.php.net/php.internals/129076 Best regards Tim Düsterhus

Arnaud.lb

300 days ago
Hi, On Wed, Nov 5, 2025 at 1:39 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> PHP has gradually been moving towards “making illegal states > unrepresentable”. With the migration from resources to objects and the > removal of the associated `_close()` functions, PHP developers and > static analysis tools can rely on the fact that having a reference to > the object means that the reference will always be valid. This is also > something that Kamil mentioned as a good thing in the RFC discussion for > the PDO::disconnect() method: > https://news-web.php.net/php.internals/128742
I'm all for making illegal states unrepresentable, and I'm glad that PHP goes in this direction. But I don't think this is achievable or desirable for objects that represent external resources like files or connection to servers, which is what with() and similar mechanisms target. These resources can become invalid or operations on them can fail for reasons that are external to the program state. Removing close() methods will not achieve the goal of ensuring that these resources are always valid.
> If $fd escapes and is nevertheless closed at the end of the block, this > may affect the program's behavior in various ways: > - Suddenly any operation on the file descriptor fails.
This will also happen due to external factors, for example if the disk becomes full. Having a File object that can not be closed doesn't ensure that operations on it will not throw. Regarding `use()`, there are two alternatives, with different outcomes: 1. use() doesn't forcibly close resources: If a resource escapes despite the intent of the programmer, the program may appear to work normally for a while until the leak causes it to fail 2. use() forcibly closes resources: If a resource escapes despite the intent of the programmer, the program may fail faster if it attempts to use the resource again The second alternative seems better to me: * If a mistake was made, the program will stop earlier and will not successfully interact with a resource that was supposed to be closed (which could have unwanted results) * Troubleshooting will be easier than chasing a resource leak
> Being able to let resource objects escape is a feature, since this > allows to reliably pass locks around without the resource suddenly > getting unlocked.
Would you utilize `use()` to lock a file in cases where the lock is supposed to outlive the `use()` block? Making objects invalid to detect bugs can also be a feature: We could make a LockedFile object invalid once it's unlocked, therefore preventing accidental access to the file while it's unlocked.
> > Escaping/capturing is difficult to avoid, especially in large code > > bases, > > as it can not be checked with static analysis, typing, or avoided by > > means > > of API design. Sometimes it's even necessary, e.g. a file descriptor > > may be > > referenced by an I/O polling mechanism. > This is true, but equally affects “not closing” and “forcibly closing” > the resource. In case of forcibly closing, your I/O polling mechanism > might suddenly see a dead file descriptor (or worse: a reassigned one) -
The reassigned case can not happen in PHP as we don't use raw file descriptor numbers.
> and static analysis tools need to report every single method call as > “might possibly throw an Exception”.
This is the case even if we removed every possible way to close a file descriptor
> PHP’s main mechanism of managing lifetimes is reference counting and by > that its semantics are much closer to those of languages that you call > “stack allocated”. Specifically PHP's semantics around resources and > objects match the semantics of `std::shared_ptr()` (C++) or `Rc` (Rust), > which - like PHP - are languages that guarantee that destructors are > predictably executed. Namely exactly when the reference count falls to > zero. > > This is also documented and thus an explicit part of the semantics that > PHP users rely on - and not just an implementation detail: > https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.destructor. > The file locking example using Seifeddine's PSL library from the RFC is > a real-world use case that successfully relies on these semantics. > > It is true that the point in time when the reference count falls to zero > is unpredictable in case of cycles, since this is dependent on the > assistance of the cycle collector. Cycles however are a comparatively > rare situation, particularly when dealing with a resource object. These > situations are also easy to resolve using the same mechanism that one > would use in C++ to deal with shared_ptr cycles, e.g. by including a > WeakReference for one of the directions.
I don't think that `use()` is an upgrade, if it means that I have to think about refcounts, track reference cycles, and carefully add WeakReferences. This seems like too low level considerations to have when programming in a high level language. This is not better than the problems it tries to fix. The fact we had to introduce a cycle collector, and that most projects don't disable it, shows that cycles exist in practice. The fact that they exist or can be introduced is enough that thinking of PHP's GC mechanism as something closer to a tracing GC is easier and safer, in general. A resource doesn't have to be part of a cycle, it only needs to be referenced by one. I don't agree that it's easy to resolve or to avoid cycles. There are no tools to discover or prevent them, and they don't show up in CI. They can be introduced at any time, so a program employing `use()` that works as expected today may break later due to an unrelated change. And it doesn't always depend on the application's own code, sometimes this happens due to a library. But cycles are not the only issue: Variables can be captured, accidentally or not (e.g. by a logger/tracer/cache/library/eventloop), without the knowledge of the programmer, increasing their refcount and extending their lifetime. Best Regards, Arnaud

Tim Düsterhus

295 days ago
Hi On 11/5/25 17:17, Arnaud Le Blanc wrote:
> But I don't think this is achievable or desirable for objects that > represent external resources like files or connection to servers, > which is what with() and similar mechanisms target. These resources > can become invalid or operations on them can fail for reasons that are > external to the program state. Removing close() methods will not > achieve the goal of ensuring that these resources are always valid.
That is correct, but I don't think that this is an argument in favor of increasing the number of these situations. Even for “unreliable” external resources, introspection functionality generally is effectively infallible (i.e. it only fails in situation where the entire system is in a bad state). Following our Throwable policy (https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables) I can meaningfully handle a “DiskFullException” when attempting to write into a file. But I handling a “FileHandleBrokenError” is not meaningful, particularly when it's something like calling `fstat(2)` which is explicitly acting on a file descriptor you are already holding.
>> 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: > > 1. use() doesn't forcibly close resources: If a resource escapes > despite the intent of the programmer, the program may appear to work > normally for a while until the leak causes it to fail > 2. use() forcibly closes resources: If a resource escapes despite the > intent of the programmer, the program may fail faster if it attempts > to use the resource again > > The second alternative seems better to me: > > * If a mistake was made, the program will stop earlier and will not > successfully interact with a resource that was supposed to be closed > (which could have unwanted results) > * Troubleshooting will be easier than chasing a resource leak
This is based on the assumption that “escapes” are always unintentional, which I do not believe is true (as mentioned in the next quoted section). 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 the `use()` 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

Arnaud.lb

295 days ago
Hi, On Sun, Nov 9, 2025 at 9:08 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> On 11/5/25 17:17, Arnaud Le Blanc wrote: > > But I don't think this is achievable or desirable for objects that > > represent external resources like files or connection to servers, > > which is what with() and similar mechanisms target. These resources > > can become invalid or operations on them can fail for reasons that are > > external to the program state. Removing close() methods will not > > achieve the goal of ensuring that these resources are always valid. > > That is correct, but I don't think that this is an argument in favor of > increasing the number of these situations. Even for “unreliable” > external resources, introspection functionality generally is effectively > infallible (i.e. it only fails in situation where the entire system is > in a bad state). > > Following our Throwable policy > (https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables) > I can meaningfully handle a “DiskFullException” when attempting to write > into a file. But I handling a “FileHandleBrokenError” is not meaningful, > particularly when it's something like calling `fstat(2)` which is > explicitly acting on a file descriptor you are already holding.
As I'm seeing it, a File object that was explicitly closed would throw an exception like "FileIsClosedError". It would indicate a lifetime bug that needs to be fixed, not something that should be handled by the program. This is reasonable, as closing is a clear intent that the resource should not be used anymore. This is not an exception that needs to be handled/checked. Under these intentions, leaving the resource open (for the reason it's still referenced) and allowing writes to it would be much worse. BTW, has the idea of removing close() methods on resources been tried successfully in other languages?
> > Regarding `use()`, there are two alternatives, with different outcomes: > > > > 1. use() doesn't forcibly close resources: If a resource escapes > > despite the intent of the programmer, the program may appear to work > > normally for a while until the leak causes it to fail > > 2. use() forcibly closes resources: If a resource escapes despite the > > intent of the programmer, the program may fail faster if it attempts > > to use the resource again > > > > The second alternative seems better to me: > > > > * If a mistake was made, the program will stop earlier and will not > > successfully interact with a resource that was supposed to be closed > > (which could have unwanted results) > > * Troubleshooting will be easier than chasing a resource leak > > This is based on the assumption that “escapes” are always unintentional, > which I do not believe is true (as mentioned in the next quoted section).
This diverges considerably from the features that the RFC claims to be designed after. Other languages with these features forcibly close the resource, while languages favoring RAII idioms make it very obvious when variables have non-local lifetimes. I feel that the RFC is taking a risk, and doesn't build on proven features, as it states. IMHO it is encouraging an idiom that comes with many pitfalls in PHP. The exception/backtrace issue demonstrated by Ed and Claude is not easily fixable with attributes or weak references, as the resource can be referenced indirectly: https://3v4l.org/lsicN Exceptions/backtraces are not the only way to capture a variable. It can happen in explicit ways: ``` using ($fd = fopen("file", "r")) { $buffer = new LineBuffered($fd); } // $fd not closed ``` Of course we can can do this instead, but it's easy to forget, so it's a pitfall: ``` using ($fd = fopen("file", "r"), $buffer = new LineBuffered($fd)) { } ``` And now, all precautions that one should take for resources (do not create cycles, do not extend lifetime) should also be taken for anything the resource is passed to. Here we capture $this by declaring a closure: ``` class CharsetConversion { function __construct(private mixed $fd, private string $from, private string $to) { if (function_exists("iconv")) { $this->convert = fn($input) => iconv($this->from, $this->to, $input); } else { $this->convert = fn($input) => mb_convert_encoding($input, $this->to, $this->from); } } function readLine() { return ($this->convert)(fgets($this->fd)); } } using ($fd = new File("file", "r"), $conversion = new CharsetConversion($fd, "iso-8859-1", "utf-8")) { } // $fd not closed ``` Async frameworks are likely to hold onto resources, by design: ``` using ($fd = ..) { await(processFile($fd)); } // $fd not closed ``` In this case a proper exitContext() / dispose() would likely request the framework to stop watching $fd.
> Managing lifetimes properly is already something that folks need to do. > You mention “file descriptor leak”, but this is no different from a > “memory leak” that causes the program to to exceed the `memory_limit`, > because some large structure was accidentally still referenced somewhere.
There are a few differences between memory and other resources: * Cycles can retain both memory and external resources, but the GC is governed only by memory-related metrics. So, cycles are usually cleared before they become a problem WRT memory usage, but not WRT other limits. I believe this is the reason why other languages chose to not rely on finalizers to release non-memory resources. * Memory is usually less scarce than other resources (files, db connections) * Releasing memory is usually less time-sensitive than other resources (locks, transactions) * Memory usage can be observed in an easier way * Resources can be released explicitly, but not memory
> > Making objects invalid to detect bugs can also be a feature: We could > > make a LockedFile object invalid once it's unlocked, therefore > > preventing accidental access to the file while it's unlocked. > > To make this same, the state of the lock object would need to be checked > before every access, which I believe is impractical and error prone. If > you forget this check, then the file might already be unlocked, since > every function call could possibly have unlocked the file by calling > `->unlock()`. > > By tying the lock to the lifetime of an object it's easy to reason about > and to review: If the object is alive, which is easily guaranteed by > looking if the corresponding variable is in scope, the lock is locked.
I would represent this as a LockedFile object, and make it an error to access the object if it has been closed/discarded, so it's not needed to check its state explicitly before every access. This is under the assumption that if I closed or unlocked the file, I don't intend it to be used anymore. If it's still accessed, letting the access go through seems worse than terminating the program.
> >> This is true, but equally affects “not closing” and “forcibly closing” > >> the resource. In case of forcibly closing, your I/O polling mechanism > >> might suddenly see a dead file descriptor (or worse: a reassigned one) - > > > > The reassigned case can not happen in PHP as we don't use raw file > > descriptor numbers. > > I was thinking about the following situation: > > - A file object is created that internally stores FD=4. > - The file object is passed to your IO polling mechanism. > - The file object is forcibly closed, releasing FD=4. > - FD=4 still remains registered in the IO polling mechanism, since the > IO polling mechanism is unaware that the file object was forcibly closed. > - A new file object is created that internally gets the reassigned FD=4. > - The IO polling mechanism works on the wrong FD until it realizes that > the file object is dead. > > Am I misunderstanding you?
I'm not sure. I would expect an I/O polling mechanism to remove the FD as soon as it's closed.
> > The fact we had to introduce a cycle collector, and that most projects > > don't disable it, shows that cycles exist in practice. The fact that > > they exist or can be introduced is enough that thinking of PHP's GC > > mechanism as something closer to a tracing GC is easier and safer, in > > general. A resource doesn't have to be part of a cycle, it only needs > > to be referenced by one. > > The data structures that tend to end up circular, are not the data > structures that tend to store resource objects.
I disagree. I gave one counter example above.
> And as outlined above, > making a variable escape the local scope needs some deliberate action.
I also disagree. It's true in C++ or Rust, as extending the lifetime of a local var would be undefined behavior or a compile time error, but not in PHP. Doing anything useful with a resource will likely involve passing it to other functions, which increases the chances of it happening. See also the examples above. Best Regards, Arnaud

Rowan Tommins [IMSoP]

295 days ago
On 10/11/2025 13:24, Arnaud Le Blanc wrote:
> 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]

Rob Landers

301 days ago
On Mon, Nov 3, 2025, at 22:46, Seifeddine Gmati 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 ``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. >
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

Rob Landers

301 days ago
On Tue, Nov 4, 2025, at 14:59, Rob Landers wrote:
> On Mon, Nov 3, 2025, at 22:46, Seifeddine Gmati 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 ``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. >> > > 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

Tim Düsterhus

301 days ago
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

Tim Düsterhus

300 days ago
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

Tim Düsterhus

299 days ago
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

Seifeddine Gmati

301 days ago
On Mon, 3 Nov 2025 at 22:46, 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 `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. > >
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.

Rowan Tommins [IMSoP]

301 days ago
On 3 November 2025 21:46:15 GMT, 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)".
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]

Tim Düsterhus

296 days ago
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. On 11/4/25 19:19, Rowan Tommins [IMSoP] wrote:
> 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

Rowan Tommins [IMSoP]

295 days ago
On 09/11/2025 16:52, Tim Düsterhus wrote:
> 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]

Tim Düsterhus

293 days ago
Hi Am 2025-11-10 11:04, schrieb Rowan Tommins [IMSoP]:
> On 09/11/2025 16:52, Tim Düsterhus wrote: >> 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, > 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)
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

Rowan Tommins [IMSoP]

293 days ago
On 11/11/2025 21:12, Tim Düsterhus wrote:
> 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.
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]

Tim Düsterhus

293 days ago
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 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. > > > 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/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.
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`, nor `static` may be used from within the `use()` block. - `static` variables defined *before* the `use()` may not be used. `static` variables defined *after* are okay, since there is no ambiguity. - `global` variables may be used. This is consistent with `unset()` 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 :-)

Tim Düsterhus

282 days ago
Hi On 11/12/25 10:53, Tim Düsterhus wrote:
>> 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

Rowan Tommins [IMSoP]

276 days ago
On 23 November 2025 14:23:40 GMT, "Tim Düsterhus" <tim@bastelstu.be> wrote:
>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
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]

Ben Ramsey

275 days ago
On 11/29/25 13:12, Rowan Tommins [IMSoP] wrote:
> 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/

Tim Düsterhus

265 days ago
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

Tim Düsterhus

282 days ago
Hi On 11/3/25 22:46, Seifeddine Gmati wrote:
> 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()` to `let()`. 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

Tim Düsterhus

282 days ago
Hi On 11/23/25 15:29, Tim Düsterhus wrote:
> 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

Tim Düsterhus

264 days ago
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. Diff is at: https://wiki.php.net/rfc/optin_block_scoping?do=diff&rev2%5B0%5D=1763913496&rev2%5B1%5D=1765398288&difftype=sidebyside Best regards Tim Düsterhus

Tim Düsterhus

257 days ago
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: https://wiki.php.net/rfc/optin_block_scoping?do=diff&rev2%5B0%5D=1765892090&rev2%5B1%5D=1766015568&difftype=sidebyside 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

Tim Düsterhus

224 days ago
Hi On 11/3/25 22:46, Seifeddine Gmati wrote:
> 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
The actual proposal of this RFC has been unchanged since the middle of November and the last meaningful change to the RFC text was made in the middle of December to improve the explanation. While the discussion in the related “comparison thread” by Rowan still has been active before the holiday break (with one reply of mine after the break), it was only discussing fundamental syntax (and semantic) choices, which we do not plan to change at this point. Both because Seifeddine and I are confident in the design of the RFC and because changing them would be equivalent to a full rewrite of the RFC (which would be more appropriate as a new v2 RFC). Even though we ultimately disagreed, we would like to thank Rowan in particular for the insightful discussion that provided valuable help to further refine and clarify the RFC text, hopefully helping folks make an informed decision. We therefore plan to open voting later this week, unless someone speaks up pointing out a major oversight or any other meaningful concerns we all missed. Best regards Tim Düsterhus