Re: [RFC] Context Managers

From: Date: Tue, 11 Nov 2025 20:20:02 +0000
Subject: Re: [RFC] Context Managers
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to internals+get-129201@lists.php.net to get a copy of this message
On 09/11/2025 21:46, Seifeddine Gmati wrote:
This is the exact reason why, in the use() thread, I suggested a Disposable interface should **not** have an enter() method. The language should just guarantee a single dispose() method is called when the scope is exited (successfully or not).
Remember, the Context Manager is not the same as the resource being managed, and exitContext() is not equivalent to dispose(). In fact, it occurs to me that a C#-style using() block can be written as a Context Manager with only a few lines of code: interface Disposable {
    public function dispose(): void;
} class Disposer implements ContextManager {
    private function __construct(private Disposable $resource) {}
    public static function of(Disposable $resource): ContextManager {
        return new self($resource);
    }
    public function enterContext(): Disposable {
        return $this->resource;
    }
    public function exitContext(): void {
        $this->resource->dispose();
    }
} Then to use it, you just pass in whatever object you want: with(Disposer::of(new MyDisposableResource) as $foo) {
    // ...
    $foo->whatever();
    // ...
} // guaranteed call to $foo->dispose() here Which is pretty close to a direct port of C#: using($foo = new MyDisposableResource) {
    // ...
    $foo->whatever();
    // ...
} // guaranteed call to $foo->dispose() here
You're right, but the use() proposal has a clear path to solve this, which is far superior IMO to the manual checks required by the ContextManager design. As discussed in the use() thread, we could later introduce: 1. A **Resource** marker interface: This would tell the engine to handle the object specially (e.g., use weak references in backtraces to prevent accidental refcount inflation). 2. A **Disposable** interface: This would have the dispose() logic and could (and probably should) also be a Resource.
Neither of these is specific to the use() block. #1 is just a general language feature - as someone else pointed out, it could be similar to #[SensitiveParameter]. #2 is, as shown above, trivial to implement using a context manager, without even needing additional language support.
Furthermore, a Disposable interface opens up the possibility of being supported *outside* the use() construct entirely. The engine could guarantee dispose() is called whenever the object goes out of *any* scope, just like a destructor but with crucial exception awareness:
This is an interesting idea, but again doesn't seem to have any special relationship to the use() proposal. It's also not what C# or Hack mean by "Disposable", so that would probably be a poor choice of name. It sounds more like an extension of the current __destruct(), which could potentially be as simple as adding a parameter to that.
With these (future) additions, the use() construct could be enhanced to **enforce a no-escape policy** specifically for Resources. If use($r = get_resource()) finishes and $r still has references, the engine could throw an error. This combination would *programmatically prevent* the "use after free" pitfall you described, rather than relying on manual checks inside every single method.
Unless the error happens at compile time (probably impossible in PHP's current workflow), or crashes the application with an uncatchable error (yikes!), I don't think it's possible to make that guarantee. Consider this code: try {
    use($r = get_resource()) {
        SomeClass::$staticVar = $r;
    }
} catch ( Throwable $e ) {
    log_and_continue($e);
} SomeClass::$staticVar->doSomething(); What is the value of SomeClass::$staticVar? I can only see three options: 1. It is still the open resource from get_resource(); the call succeeds, but the resource has leaked 2. It is a closed resource, and the call to doSomething() throws an "object already disposed" Error 3. It has been forcibly unset, and the call to doSomething() throws a "method call on null" Error (I don't think #3 is actually possible in the current Engine, because we only track the reference *count*, not a reference *list*; but it's at least theoretically possible.) And that's leaving aside the fact that a lot of resources can end up in unusable states anyway, such as a network connection being closed from the other end.
All of those powerful safety checks can be added in the future. I don't see why we need to cram them into the initial use() RFC.
Requiring a "double opt-in" (the use() block and an interface) makes the feature less reliable - you can't see at a glance if the use() block is performing the extra cleanup, or just creating a variable scope. I think it's better to have a specific block that can *only* be used with objects implementing the appropriate interface, like using+IDisposable (C# and Hack) or try+AutoCloseable (Java https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html). If we want a general-purpose "block scoping" feature alongside that, we can discuss exactly how that should look and operate, as I replied to Tim here: https://externals.io/message/129059#129188 -- Rowan Tommins [IMSoP]

Thread (33 messages)

« previous php.internals (#129201) next »