[PHP-DEV] [RFC][Discussion] use construct (Block Scoping)

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.

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:

// I know for sure that Scope implements the interface
// required to be used with "with"
with $scope = new Scope() {}

// I have no idea whether the File class implements
// the required interface or not. It’s unclear what will happen in the end.
with $file = new File("...") {}

So, in Python you cannot use arbitrary objects in a with statement,
only those that implement the __enter__ and __exit__ contract.

Hello Ed,

Thank you for your feedback. Regarding your concern about the clarity
when using a `use` statement with objects that may or may not
implement a `Disposable` interface, it does not matter to the
application developer whether a future `Disposable` interface is
implemented or not.

Consider this example:

// PHP Builtin:
interface DisposableInterface {
   public function dispose(?Throwable $error): void;
}

// Library Code:
interface DatabaseTransaction extends DisposableInterface {
  public function execute(string $q): void;
}

interface DatabaseConnection {
   public function beingTransaction(): DatabaseTransaction;
}

interface DatabasePool {
   public function getConnection(): DatabaseConnection;
}

// Application Code:
function do_work(DatabasePool $pool): void {
  using (
    $connection = $pool->getConnection(),
  ) {
    using ($transaction = $connection->beingTransaction()) {
      $transaction->execute('...');
      sleep(10); // more work.
      $transaction->execute('...');
    }

    sleep(10); // more work
  }

  sleep(10); // more work
}

In this scenario, the library author might not implement `Disposable`
for the `DatabaseConnection` because its internal handle is
automatically closed on `__destruct`, so to them, `Disposable` adds no
value. However, for the `DatabaseTransaction`, they do implement it,
as it allows the transaction to commit or rollback based on the exit
status.

From the application developer's perspective, both are temporary
resources that are "allocated" and will be disposed of after the
scope. How they are disposed of is decided by the maintainer of that
resource (in this example, a third-party library). They might feel
`__destruct` is sufficient (e.g., for a socket to be closed), or they
might feel the need for `Disposable` to perform a specific action
based on whether the operation finished successfully.

Thanks,
Seifeddine

Hello!

function addStudentLessons(DatabaseTransaction $transaction) {
     try {
          $transaction->execute(...);
     } catch(\Exception $e) {
         Logger::log($e);
         throw $e;
     }
}

// Application Code:
function do_work(DatabasePool $pool): void {
  using (
    $connection = $pool->getConnection(),
  ) {
    using ($transaction = $connection->beingTransaction()) {
      addStudentLessons($transaction);
      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.

P.S.

In other words:

Disposable cannot replace __destruct, because it solves a different problem. it provides deterministic cleanup, while __destruct relies on refcount semantics and may be delayed.

-–
Best Regards, Ed

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

Le 6 nov. 2025 à 06:01, Edmond Dantes edmond.ht@gmail.com a écrit :

Hello!

function addStudentLessons(DatabaseTransaction $transaction) {
try {
$transaction->execute(...);
} catch(\Exception $e) {
Logger::log($e);
throw $e;
}
}

// Application Code:
function do_work(DatabasePool $pool): void {
using (
$connection = $pool->getConnection(),
) {
using ($transaction = $connection->beingTransaction()) {
$transaction->execute('...');
sleep(10); // more work.
$transaction->execute('...');
}

sleep(10); // more work
}

sleep(10); // more work
} // <== broken!

Logger::log($e); <== reason!

In this example, the 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

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

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):

Online PHP editor | output for 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 (Online PHP editor | output for tL5Yt):

     $local_lock = $lock; unset($lock);

I'm not trying to say that this is obvious - I agree that Exceptions capturing parameters is something that folks need to learn about - , but it is a possible solution to this problem.

Given that I don't agree that Disposable is the solution to the “Exception” issue, it's my responsibility to offer an alternative and I would like to do this:

The addition of either an attribute or a marker interface for resource objects that the backtrace capturing will observe - similarly to the #[\SensitiveParameter] attribute. When an object is marked as a resource object, then it will be wrapped into a WeakReference when the backtrace is captured. As a result, the object will remain accessible for backtrace processing / logging as long as it would still be alive if there wasn't an Exception. But it will prevent the Exception from unexpectedly extending the lifetime in other cases.

In addition I could imagine a WeakReference storing the class name of the originally stored object as a “poor man's generic”, allowing folks to learn which type of object was originally stored in the weak reference, even when the original object is already gone.

Best regards
Tim Düsterhus

Hello.

I'm not trying to say that this is obvious - I agree that Exceptions

I only gave one of many examples that will inevitably occur if you
rely on reference counting.
My main point was something entirely different.
If the tool chosen to solve the problem is not well suited, then no
matter what methods are used to avoid mistakes,
it turns into an ongoing struggle that only adds complexity and
increases the chance of error.

---
Best regards
Ed

Hi

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

Hello.

If the stated problems are a problem in practice or if they are mostly theoretical.

Accidental retention of objects in unpredictable situations is a
constant issue for applications that do not terminate after each
request.

Possible solutions to the problems (such as the “WeakReference capturing of arguments”).

As a rule, any error can be fixed, but that is not an argument in this
situation. A developer can certainly solve a problem if they are aware
of it. The issue is that if you rely on references, they might never
even realize it exists :slight_smile:
The problem is not fixing the error, but how easy it is to detect it.

If the described problem even exists or if it is based on a misunderstanding of either the RFC or PHP's existing behavior.

Wasn’t the example above convincing?

---
Best regards
Ed

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 (policies/coding-standards-and-naming.rst at main · php/policies · GitHub) 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

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]

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
(policies/coding-standards-and-naming.rst at main · php/policies · GitHub)
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:

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

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.

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]

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

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: Online PHP editor | output for 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: Online PHP editor | output for VBVkX
- Somewhat unusually, they do this as run-time statements, so you can *conditionally* shadow variables: Online PHP editor | output for fK9VJ

Whether these are *good* semantics to copy for a block-scoped variable, I'm not sure; but they are existing PHP semantics for a very similar situation.

I'm not aware of any language that requires a specific kind of block in order to introduce a new scope, but that doesn't mean it's a bad idea. The approaches I am aware of are:

The closest comparison to “specific kind of block” might perhaps be older versions of C which require all variables to be declared - and for them to be declared at the top of the scope without any logic running in-between.

The big difference is the need for an extra level of indent (or, at least, an extra pair of braces, which most people will probably assume needs an extra level of indent). More often than not, there is an existing block you want to scope to - a particular loop, or a conditional branch, etc.

I do see the advantage of forcing them to the start, though. Languages in the Pascal family might be another comparison to explore - variables are all declared in a separate block at the top of a function. Off-hand, I'm not sure if any allow an arbitrary nested block just to introduce additional variables.

Regards,

--
Rowan Tommins
[IMSoP]

Hi

Am 2025-11-11 23:43, schrieb Rowan Tommins [IMSoP]:

That's fair. I'll look into adding more explicit comparisons to the RFC together with Seifeddine. My short answer here would be that JavaScript already had explicit scoping by means 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: Online PHP editor | output for 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: Online PHP editor | output for VBVkX
- Somewhat unusually, they do this as run-time statements, so you can *conditionally* shadow variables: Online PHP editor | output for 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: Online PHP editor | output for 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: Online PHP editor | output for 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 :slight_smile:

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”:

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

Hi

On 11/3/25 22:46, Seifeddine Gmati wrote:

Please find the following resources for your reference:

    -

    RFC: PHP: rfc:optin_block_scoping
    -

    POC:
    Comparing php:master...TimWolla:block-scope · php/php-src · GitHub

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

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