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

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:

Thanks,

Seifeddine Gmati.

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

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

What would happen to the variables introduced in the statements block, rather than in use()? Will they still be available outside of the block?

use () {

$number = 42;
}
var_dump($number); // what happens here?

Also, is an empty list of vars in use (as in the example above) allowed?

···

Best regards,
Bruce Weirdan mailto:weirdan@gmail.com

Hi

Am 2025-11-04 07:08, schrieb Bruce Weirdan:

What would happen to the variables introduced in the statements
block, rather than 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.

use () {
  $number = 42;
}
var_dump($number); // what happens here?

Also, is an empty list of vars in `use` (as in the example above) allowed?

That is a syntax error (unexpected `)`).

Best regards
Tim Düsterhus

Hello all!

Thank you for the RFC, it has been missing for many years.
If I understand correctly, are you proposing to call `unset` at the
end of the block?

I see that the **Future Scope** section mentions `Disposable`.
But if your goal is to introduce behavior based on `Disposable`,
wouldn’t that conflict with the logic of the current RFC?
I see a clear pitfall here.

If you accept this RFC with the `unset` operation, you will later need
a new keyword for `Disposal`, because these are two entirely different
scenarios.
(Should I explain why?)

In this context, I also see a problem, as if the RFC is trying to
introduce two different features into the language:

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

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

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:

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:

Thanks,

Seifeddine Gmati.

1 Like

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:

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

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:

Thanks,

Seifeddine Gmati.

Hello,

One thing that isn’t clear with this RFC is what happens to things redefined in the scope?

$a = 10;

use ($a = 5) {
var_dump($a);
}

var_dump($a); // unset or 10?

— Rob

lol, I just saw “open issues” … so, never mind. I like option B.

— Rob

Hi

Am 2025-11-03 22:46, schrieb Seifeddine Gmati:

   How do you feel about the questions raised in the "Open Issues" section?

Given the immediate and clear unanimous responses preferring option B (restoring the original values), this is something we'll go with. I'll look into updating the implementation later this week and we'll then update the RFC based on the insights coming out of the implementation (e.g. the exact semantics and possible edge cases).

With regard to the naming, which also got multiple replies in favor of not giving `use()` yet another purpose, Seifeddine is currently running analysis on a large number of composer packages to determine the impact of various possible alternative names (let, scope, using).

Best regards
Tim Düsterhus

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: PHP: rfc:optin_block_scoping

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

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: GitHub - azjezz/php-syntax-analyzer: A simple php syntax analyzer

  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.

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/&gt; 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]

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:

interface Disposable {
  public function dispose(?Throwable $throwable = null): void;
}

With `use`:

use ($foo = new Something()) {
  // work
} // ->dispose(null) called on success, ->dispose($exception) on failure

This mimics Python's context manager protocol. The `dispose()` method
would be called before `__destruct`, allowing objects to distinguish
between successful completion and failure.

However, there's nothing stopping us from shipping without it. The
lock example in the RFC doesn't need this, locks are freed
automatically in `__destruct`. The initial version relying solely on
`__destruct` works fine for most use cases.

**On the name "Disposable"**: I'm not really a fan of this name
myself. It was just my initial thinking when trying to copy Hack. We
can come up with something better later.

**On Hack's approach**: Hack has a `Disposable` interface, but it
works differently: disposable objects must maintain refcount=1, can't
be assigned to properties, and functions returning disposables need
`<<__ReturnDisposable>>`. This is enforced statically by their
analyzer. At runtime, they're just regular PHP objects. We can't
replicate this in PHP.

**The refcount problem**: A disposal interface has its issues. Once
`dispose()` is called, there may still be references to the object
somewhere, leaving it in an undesirable state, unless we add a way to
enforce refcount = 1.

A disposal interface without exception awareness brings limited value.
But with `?Throwable`, it becomes useful:

use ($transaction = $ctx->beginTransaction()) {
  // work
} // Transaction::dispose(?Throwable) called, then __destruct

Without `dispose(?Throwable)`, the transaction can't know whether to
commit or rollback, `__destruct` alone can't distinguish success from
failure.

**The key point**: the initial version (relying on `__destruct`) and a
future disposal interface don't conflict.

Thanks,
Seifeddine Gmati.

Hello

A disposal interface has its issues.

Although Arnaud Le Blanc has already covered this topic thoroughly,
I’d like to approach it from a slightly different angle.
Let’s not think of __enter__/__exit__ + RefCount as a problem. Every
approach has its own purpose. In this case, there is a clear
distinction between Scope logic and __enter__/__exit__ logic. These
are two different concepts, and you cannot and should not try to
satisfy both RefCount and __enter__/__exit__ conditions at the same
time.

The purpose of __enter__/__exit__ is to handle the try-catch-finally
pattern for a resource regardless of the reference count. Therefore,
RefCount is not an issue.
(although PHP can automatically issue a warning when attempting to
call the method while the reference count is greater than one)

If PHP applies unset or __enter__/__exit__ depending on whether an
interface is implemented, it will introduce hidden behavior in the
code, making it harder for developers to understand what is happening.
Compare the two cases:

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

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

So, in Python you cannot use arbitrary objects in a with statement,
only those that implement the __enter__ and __exit__ contract.
Therefore, in Python there is no ambiguity in the code. The developer
understands that if `with` is used, it means the object definitely
implements the required interface, otherwise an error will occur. PHP
must guarantee the same behavior.

P.S.
If I’m not mistaken, a recent RFC was proposed about context managers,
which covers exactly this logic.

---
Ed

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: PHP: Constructors and Destructors - Manual. 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(...);
}

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: php.internals: Re: [RFC][Discussion] use construct (Block Scoping)

// 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: php.internals: Re: [Discuss] Add PDO disconnect() and isConnected()

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: php.internals: Re: [RFC][Discussion] use construct (Block Scoping)

Best regards
Tim Düsterhus

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 (php.internals: Re: [RFC][Discussion] use construct (Block Scoping)) and my reply to Arnaud that I just sent (php.internals: Re: [RFC][Discussion] use construct (Block Scoping)).

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

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:
php.internals: Re: [Discuss] Add PDO disconnect() and isConnected()

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:
PHP: Constructors and Destructors - Manual.
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

1 Like

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

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]