Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:129185 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 591D51A00BC for ; Sun, 9 Nov 2025 21:46:31 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1762724795; bh=WMhVLac4jv9PQmKb9TQu2ryVJ0PTuZv7munoKYqAKPA=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=TWjGw9NDNUPxycimFNHX3Qb6P5BYZ83pHKVYOBWBPaRtmZ0TWcg5yT/JUbZVxtoCN 0+nDowjcHfyYdc7KOTUxkl8boHUGpITExjQIjbJJslMv5wbCdGNvBsPa0CZsFcQhQ/ rTu6+5G0IYPCAMl7dq3WkC/qGnPlTJ6CpVedZXzDpKqOz+QJXFRj2UvxtKdil24wNl 0P3ItkqkkWmBA7WT8as8PJx2dLi+9v6GNMkJVZCdsMY4GPjareKjlFu3vXctVw2ola Bf3FxoOgG+cB7UYYHjr/6xdweqjLA8UZ+maHKl23+ucQGRocCCnXkj4cS3XjAka6uI XH+6Bejky5BKg== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 617231801EF for ; Sun, 9 Nov 2025 21:46:34 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=0.8 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DMARC_MISSING,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2, SPF_HELO_NONE,SPF_NONE autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from mail-vs1-f43.google.com (mail-vs1-f43.google.com [209.85.217.43]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Sun, 9 Nov 2025 21:46:34 +0000 (UTC) Received: by mail-vs1-f43.google.com with SMTP id ada2fe7eead31-5d96756a292so2014034137.0 for ; Sun, 09 Nov 2025 13:46:29 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=carthage-software.20230601.gappssmtp.com; s=20230601; t=1762724788; x=1763329588; darn=lists.php.net; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=JeXA9Iis5x8YEoid6NOkzdM+bWHvtHXKEem/ixbrtOY=; b=bYprqjsTwkNayrkIkIwMN6PzlPbwUKAKp3Nm3uvkRDctr2OqvFmRqKauGL42L8LqsQ aBRHUMx4aoDPbzHFEk1U5dLYmf5QkEocT2fUOF/8QGv/bks8btP1jzZI9DCeiI3eEhi7 UD9phtKlwunBcEn5kyD1dixcCiTnffpLNHrcwk1uqR1wMXTxjq1UOubvhLPJYRWhxmfm W4KOWR0M+s5YAaW3Zp+naY7DuEnjVqgi7nCr6BrexcFxC49GcvwZDTPqe9LumUJ98T/2 emsQJRCrc2iZWN69CjCneIGNP4HV5JMfZPNK+cmtZ/98zMY6Z3YmeJiP6RZ96glaZow0 KZrg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1762724788; x=1763329588; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=JeXA9Iis5x8YEoid6NOkzdM+bWHvtHXKEem/ixbrtOY=; b=p0qb1jJIsI8021pyAnLSVXrN4Jl/pdElPibqecAWRPzSP/rvCvTUiDcdd+3/J51RXS qr5n1npOyVk+JEUTQ3ZdEL6u/cMvWqzjTsajibGTmeMezbRLw1KZFK1iyjQqdoa7qxxr EIbDqTTON/AiH490Hl2dbB0BKhSALU16VKkOGQrJX8A/2G5kY2IwLnQMN2j9BRjAkDuc OuphAZYlpg7gOpyIYMl3X6sj1TmamN6rJX38jQSYwAqsnMITIDR8BT+4nHwo02LIv6w9 eynlK1uwf5QAdyg7Z0xj3vNTAjkTy+kWaXit7vIXcWaqPqYYg2w//mgvoANDkw15+vOy fAXw== X-Forwarded-Encrypted: i=1; AJvYcCWgp/pJ6uU111Lq6niTk37wrKTHqFerA+eg++mCAg4avmYI3OjzEyN+zODIwAaY9zu0lnkgybGHrgg=@lists.php.net X-Gm-Message-State: AOJu0Yyz2wLGzsnOvQxocVHlsf7ylm4yX0Uyq0zcZ2ElcFPjCAeRQDgT 15k+Z/1zH2n5rHMtrJWtnKvfRJZTLhrzgkfh5nALJ+TAzfwIs9Rb4G4SXHRCRinhKJDDbjRGaca ba2heGhR44Qja0iM086zyEAr7TuMoy8pM52Kmxn1ygg== X-Gm-Gg: ASbGnctVXnXJ24pcLn0nH9E63pRoqE+4KJX/9Nanhv1KBG0yAyhBDJdYTmY+hqyH59c tYQ7n2Ns79Bk/OXuJHNa7HFLGa3w/4oKP63adc+Rd3zM01W/AazskC8bLuXl2r7EjlfceZBj8Aw yojmxcmgkOI1d55Q4KV4ntSf83dV45jGNjXb4FZk+vwl36HAAaYwdJioL6hGncWP2Ng7HTyBh3l r6/sxnQJw2cxnnzeY7IBge9d94gTh9NB1ppffATVe2RrHhvFGqyv7uTYFYwXpWpuYiXyU1k X-Google-Smtp-Source: AGHT+IHqs5ZWtIOf4NaDj5hRm5qDtoU2N+ubTve53d9BoVK7ieSYbcsyqHipuNv0B2tIZXm47rZO2vt2OSl4VHg8O+A= X-Received: by 2002:a05:6102:5491:b0:5a5:57f0:f426 with SMTP id ada2fe7eead31-5ddb9afd80bmr2990464137.5.1762724788022; Sun, 09 Nov 2025 13:46:28 -0800 (PST) Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 References: In-Reply-To: Date: Sun, 9 Nov 2025 22:46:17 +0100 X-Gm-Features: AWmQ_blI2AZqFwoquRTbJBGA3Oyh2mGjpSFDJFpcCchsGZQ212BDGqBjl-vbcdE Message-ID: Subject: Re: [PHP-DEV] [RFC] Context Managers To: Bob Weinand Cc: Larry Garfield , php internals , =?UTF-8?Q?Tim_D=C3=BCsterhus?= , Arnaud Le Blanc Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable From: azjezz@carthage.software (Seifeddine Gmati) On Wed, 5 Nov 2025 at 23:39, Bob Weinand wrote: > > Hey Larry, Tim, Seifeddine and Arnauld, > > On 4.11.2025 21:13:18, Larry Garfield wrote: > > Arnaud and I would like to present another RFC for consideration: Conte= xt Managers. > > > > https://wiki.php.net/rfc/context-managers > > > > You'll probably note that is very similar to the recent proposal from T= im and Seifeddine. Both proposals grew out of casual discussion several mo= nths ago; I don't believe either team was aware that the other was also act= ively working on such a proposal, so we now have two. C'est la vie. :-) > > > > Naturally, Arnaud and I feel that our approach is the better one. In p= articular, as Arnaud noted in an earlier reply, __destruct() is unreliable = if timing matters. It also does not allow differentiating between a succes= s or failure exit condition, which for many use cases is absolutely mandato= ry (as shown in the examples in the context manager RFC). > > > > The Context Manager proposal is a near direct port of Python's approach= , which is generally very well thought-out. However, there are a few open = questions as listed in the RFC that we are seeking feedback on. > > > > Discuss. :-) > > I've been looking at both RFCs and I don't think either RFC is good > enough yet. > > > As for this RFC: > > It makes it very easy to not call the exitContext() method when calling > enterContext() manually. The language (obviously) doesn't prevent > calling enterContext() - and that's a good thing. But also, it will not > enforce that exitContext() gets ever called (and it also cannot, > realistically). > > Thus, we have a big pitfall, wherein APIs may expect enterContext() and > exitContext() to be called in conjunction, but users don't - with > possibly non-trivial side-effects (locks not cleared, transactions not > committed etc.). Thus, to be safe, implementers of the interface will > also likely need the destructor to forward calls to exitContext() as > well. But it's an easy thing to forget - after all, the *intended* usage > of the API just works. Why would I think of that, as an implementer of > the interface, before someone complains? > > Ultimately you definitely will need the capability of calling > enterContext() and exitContext() manually too (i.e. restricting that is > not realistic either), as lifetimes do not necessarily cleanly nest - as > a trivial example, you might want to obtain access to a handle which is > behind a lock. You'll have to enter the context of the lock, enter the > context of the handle, and close the lock (because more things are > locked behind that lock, including the handle). ... But you don't > necessarily want the hold on the lock to outlive the inner handle. In > short: The proposed approach only allows nesting contexts, but not > interleaving them. > > > Further, calling with() twice on an object is quite bad in general. But > it might easily happen - you have a function which wants a transaction. > e.g. function writeSomeData(DatabaseTransaction $t) { with ($t) { > $t->query("..."); } }. A naive caller might think, DatabaseTransaction > implements ContextManager ... so let's wrap it: with($db->transaction() > as $t) { writeSomeData($t); }. But now you are nesting a transaction, > which may have unexpected side effects - and the code probably not > prepared to handle it. So, you have to add yet another safeguard into > your implementation: check whether enterContext() is only active once. > ... Or, maybe a caller assumes that $t =3D $db->transaction(); with ($t) = { > $t->query("..."); } with ($t) { $t->query("..."); } is fine - but the > implementation is not equipped to handle multiple calls to enterContext()= . > > > Additionally, I would expect implementers to want to provide methods, > which can be called while the context is active. However, it's not > impossible to call these methods without wrapping it into with() or > calling the enterContext() method explicitly. One more failure mode, > which needs handling. > Like for example, calling $t->query() on a transaction without starting i= t. > > > I don't like that design, which effectively forces you to put safety > checks for all but the simplest cases onto the ContextManager > implementation. > And it forces the user to recognize "this returned object > DatabaseTranscation actually implements ContextManager, thus I should > put it into with() and not immediately call methods on it". (A problem > which the use() proposal from Tim does not have by design.) > > > The choice of adding the exception to the exitContext() is interesting, > but also very opinionated: > > - It means, that the only way to abort, in non-exceptional cases, is to > throw yourself an exception. And put a try/catch around the with() {} > block. Or manually use enterContext() & exitContext() - with a fake "new > Exception" essentially. > - Maybe you want to hold a transaction, but just ensure that everything > gets executed together (i.e. atomicity), but not care about whether > everything actually went through (i.e. not force a rollback on > exception). You'll now have to catch the exception, store it to a > variable, use break and check for the exception after the with block. > Or, yes, manually using enterContext() and exitContext(). > > > It feels like with() is designed to be covering 70% of the use cases, > with a load of hidden pitfalls and advanced usage requiring manual > enterContext() and exitContext() calls. It's not a very good solution. > > > As to the destructors (and also in reply to that other email from > Arnauld talking about PDO::disconnect() etc.): > > It's already possible today to have live objects which are already > destructed. It's extremely common to have in shutdown code. It's > sometimes a pain, I agree. But it's an already known pain, and an > already handled pain in a lot of code. > If your object only knows "create" and "destruct", there's no way for a > double enterContext() (nested or consecutive) situation to ever happen. > (Well, yes, you *could* theoretically manually call __destruct(), but > why would you ever do that?) > > > Last thing - proper API usage forces you to use that construct. > > > To the use() proposal from Tim: > > This proposal makes it very simple to inadvertently leak the use()'d > value. I don't think the current proposed form goes far enough. > > However we could decide to force-destruct an object (just like we do in > shutdown too). It's just one single flag for child methods to check as > well - the object is either destructed or not. We could also trivially > prohibit nested use() calls by throwing an AlreadyDestructedError when > an use()'d and inside destructed object crosses the use() boundary. > > The only disadvantage is that there's no information about thrown > exceptions. I.e. you cannot add a default behaviour of "on exception, > please do this", like rolling transactions back. But: > - Is it actually a big problem? Where is the specific disadvantage over > simply $db->transaction(function($t) { /* do stuff */ }); - where the > call of the passed Closure can be trivially wrapped in try/catch. > - If yes, can we e.g. add an interface ExceptionDestructed { public bool > $destructedDuringException; }? Which will set that property if the > property is still undefined - to true if the destructor gets triggered > inside ZEND_HANDLE_EXCEPTION. To false otherwise. And, if an user > desires to manually force success/failure handling, he may set > $object->destructedDuringException =3D true; himself as a very simple - > one-liner - escape hatch. > > > The use() proposal is not a bad one, but I feel like requiring the RC to > drop to zero first, misses a bit of potential to save users from mistakes= . > The other nice thing about use() is that it's optional. You don't have > to use it. You use it if you want some scoping, otherwise the scope is > simply the function scope. > > > To both proposals: > > It remains possible by default to call methods on the object, after > leaving the with or use block. So some checking on methods for a safe > API is probably still required. > > I don't think it's possible to solve that problem at all with the > ContextManager RFC, except manual checking by the implementer in every > single method. But it's possibly possible to solve it with the use() RFC > in orthogonal ways - like a #[ProhibitDestructed] attribute, which can > be added onto a class (making it apply to all methods) or a specific > method and causes method calls to throw an exception when the object is > destructed. > Which is possible to provide by the language, as the language knows > about whether objects are already destructed, unlike e.g. the > ContextManager, where it would be object state, which has to be > maintained by the user. > > > TL;DR: ContextManagers are a buttload of pitfalls. use() is probably > better, with much less inherent problems. And with the remaining > problems of the proposal being actually solvable. > > > Thanks, > > Bob > Hi Bob, I agree with your points, especially this one: > It makes it very easy to not call the exitContext() method when calling e= nterContext() manually. [...] Thus, we have a big pitfall... 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). This completely avoids the "unbalanced call" pitfall. SA tools can warn/error when `dispose()` is called manually. API authors should be aware that the resource might receive multiple `dispose()` calls and handle this gracefully. ----- Regarding your other valid concerns (nested calls, using objects after the block, "leaking" the variable), I believe those are all solvable by how the `use()` construct evolves, which is why I prefer its foundation. You noted: > It remains possible by default to call methods on the object, after leavi= ng the with or use block. So some checking on methods for a safe API is pro= bably still required. 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`. 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: ```php function x() { $disposable =3D new SomeDisposable(); return; // $disposable->dispose(null) called } function y() { $disposable =3D new SomeDisposable(); throw new Error(); // $disposable->dispose($error) called } ``` This would make `Disposable` a truly powerful, general-purpose RAII mechanism, not just a feature tied to `use()`. With these (future) additions, the `use()` construct could be enhanced to **enforce a no-escape policy** specifically for `Resource`s. If `use($r =3D 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. 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. Its current `__destruct`-based implementation is **already useful today** for APIs designed to leverage RAII (like the Psl lock example). It's normal that few APIs are designed this way now; the feature doesn't exist yet. Psl just happens to support it because of its Hack origins. This seems like a much safer and more extensible path.