====== PHP RFC: Your Title Here ======
* Version: 2.0
* Date: 2026-10-23
* Author: Larry Garfield (larry@garfieldtech.com), Arnaud Le Blanc (arnaud.lb@gmail.com)
* Status: Draft
* Implementation:
* Discussion thread:
* Voting thread: tbd
===== Introduction =====
Context Managers are a concept borrowed from Python. They provide a way to abstract out common control-flow and variable-lifetime-management patterns, leading to vastly simplified "business logic" code that can skip over a great deal of boilerplate. They place certain guards around a "context" (block), such that certain behaviors are guaranteed upon entering and leaving the block.
Common use cases include file management and database transactions, although there are many more.
As a canonical example, this code for robust file handling that includes all necessary error management:
$fp = fopen('file.txt', 'w');
if ($fp) {
try {
foreach ($someThing as $value) {
fwrite($fp, serialize($value));
}
} catch (\Exception $e) {
log('The file failed.');
} finally {
fclose($fp);
}
}
unset($fp);
Can be simplified to this, using a context manager common to all similar cases:
using (file_for_write('file.txt') => $fp) {
foreach ($someThing as $value) {
fwrite($fp, serialize($value));
}
};
// At this point, we're guaranteed that $fp has closed, whether there was an error or not.
The semantics of this proposal draw very heavily on [[https://peps.python.org/pep-0343/|Python]], though similar, if less robust, functionality also exists in [[https://useful.codes/working-with-context-managers-in-c-sharp/|C#]] and [[https://useful.codes/working-with-context-managers-in-java/|Java]], under the same name.
This v2 proposal shares most of the same concepts and goals as the previous [[context-managers|ontext Managers]] RFC, but takes a different approach to the implementation that offers greater flexibility and obviousness.
===== Proposal =====
This RFC introduces a new keyword, ''using'', which defines a "context block" in which certain behaviors are guaranteed to happen at the start, and at the end. ''using'' is an expression, and thus may be used anywhere an expression is legal and be terminated by a semi-colon.
The syntax for ''using'' is as follows:
$RESULT = using ((GENERATOR [=> VAR])[,]+) {
BODY
};
Where:
* GENERATOR is any arbitrary expression that evaluates to a Generator object. This is also known as the "Context Manager."
* VAR is variable name that will be assigned the value yielded by the Generator. This is known as the "context variable."
* BODY is any arbitrary collection of PHP statements.
* $RESULT is an optional variable that will capture the value returned by the Generator. If the Generator does not return a value, ''using'' evaluates to null.
The intent is to "factor out" the common setup and tear down processes from various use cases to the context manager object.
A single ''using'' expression supports an arbitrary number of context manager clauses, though in most cases only a single one will be needed. (See the relevant section below.)
As a short-hand, a single-statement body may be shortened to just an expression, like so:
$foo = using(gen() => $bar) print $bar;
The Generator may execute arbitrary code. In practice, the most common pattern will be to wrap a common try-catch block around the BODY.
function cm($someValue)
{
try {
yield $someValue;
catch(SomeException $e) {
// Error handling cleanup here.
}
finally {
// General cleanup here.
}
}
However, that is not the only use case. The examples section lays out alternative uses that do not leverage try-catch-finally, but still provide guaranteed-wrapping behavior.
==== Behavior ====
When a ''using'' expression is encountered, the GENERATOR is first evaluated to ensure it is a Generator object. If it is not, an Error is thrown. (See the section below regarding resources, however.)
The Generator is then invoked, and runs any arbitrary code until it yields a value. The yielded value is assigned to VAR. If no VAR is specified, the yielded value is ignored. If no value is yielded but a VAR is specified, an Error will be thrown.
The BODY will then execute as normal. BODY runs in the scope of the function it is in. When the end of BODY is reached, execution passes back to the Generator at the point of the yield.
The Generator then continues execution until it terminates normally. If a second yield is encountered at runtime, an Error will be thrown. That is, yielding multiple times from a Generator that is used as a Context Manager is explicitly not supported. (However, this cannot be detected at compile time as multiple instances of a yield keyword doesn't imply both will actually be reached.)
If the Generator returns a value, the ''using'' expression evaluates to that value. If it does not, the ''using'' expression evaluates to null.
If an exception is thrown within BODY, it will propagate through the Context Manager generator, and thus be catchable by any catch or finally blocks it may contain.
A return statement inside the BODY will return from the function the code is currently executing in. A finally block in the generator context manager will still be executed, but otherwise control returns to the function's caller.
When the ''using'' expression ends, VAR is explicitly unset().
==== Nested context managers ====
As noted above, a ''using'' expression may include multiple context manager expressions, separated by commas. The following is entirely legal syntax:
using (foo() => $foo) {
using (bar() => $bar) {
// Code that uses $foo and $bar here.
};
};
However, it is equivalent to this, more compact form:
using (foo() => $foo, bar() => $bar) {
// Code that uses $foo and $bar here.
};
The authors are undecided as to what the return value of a combined ''using'' block should be. The options include:
- The return value of the outer-most / left-most context manager.
- An array of the return values of all context managers, in left-to-right order.
The authors request input from the rest of the community as to the best approach.
==== Mixing with try-catch ====
In practice, we expect it be reasonably common that a ''using'' expression will also need a ''try-catch'' block wrapped around it. Not in all cases, but certainly in many. For that reason, as a short-hand ''using'' may be placed inline with the ''try'' statement, like so:
try using (manager() => $var) {
print "Hello world\n";
} catch(Exception $e) {
// ...
}
That is logically equivalent to:
try {
using (manager() => $var) {
print "Hello world\n";
};
} catch(Exception $e) {
// ...
}
This approach has a few advantages:
* It doesn't require any changes to the rules for ''try''. If no extra ''catch'' or ''finally'' blocks are necessary, then the ''try'' can be omitted entirely as well.
* If the one-off ''catch'' block had "first dibs" on the bubbling exception, then it would be a common problem to not rethrow the exception, leading to the context manager generator not receiving an exception and thinking the process was successful. This way, the context manager generator is always able to respond to an exception, even if it then passes it on or rethrows it.
If the one-off ''catch'' needs first-dibs on the exception (because it requires the context variable), the entire ''try-catch'' may be placed within the ''using'' body and can rethrow or not, as appropriate.
However, to avoid needing to convert a ''try'' statement into an expression, any return value from a ''using'' block is discarded in this instance.
==== Resources ====
Resources are an interesting case. They are one of the most common things we would want to ensure are closed at a certain point. Currently, it is difficult to ensure a resource is closed properly, as the value may be assigned to another variable, or captured in an a backtrace or logger, or various other code paths that prevent "variable out of scope so close" behavior from happening. Context managers provide a straightforward and effective solution for that.
However, resources are not objects, yet. Efforts to convert them are ongoing, but not yet complete. In particular, file handles are still resources, not objects, yet are one of the most common targets for a context manager.
For that reason, resource variables are special-cased to be automatically wrapped into a generic context manager. Fortunately, all resources can be closed with a consistent interface from C, making this possible. The wrapping manager is approximately equivalent to:
function resourceContext($resource): \Generator
{
try {
yield $resource;
} finally {
close($resource);
}
}
Which enables:
// This code
using (fopen('foo.txt', 'r') => $fp) {
fwrite($fp, 'bar');
};
// Will operate equivalent to this:
using (resourceContext(fopen('foo.txt', 'r')) => $fp) {
fwrite($fp, 'bar');
};
As a reminder, the context manager and context variable are typically separate values. So ''$fp'' here is the same resource value we're used to, and thus compatible with ''fwrite()'' et al.
Naturally anyone can write their own context manager that wraps a resource and use that instead if the situation calls for it.
==== Regarding types ====
The main advantage of this approach over the previous ''ContextManager'' interface approach is its simplicity: The "surface area" is smaller, literally just a ''yield'' statement. All other behavior (how exceptions propagate, how to handle them selectively, etc.) is simply an application of existing PHP syntax and patterns that have been around for years.
The one downside is that Generators in PHP core currently have very poor support for typing. The return type of the generator function must always be simply \Generator, with no way to indicate what type will be yielded or returned.
This is a pre-existing limitation of Generators, and not created or exacerbated by this RFC. Furthermore, the ecosystem already has a workaround supported by static analysis tools. Specifically, most SA tools treat generators as the following pseudo-generic:
class Generator extends Traversable
Which can be used like so:
/**
* @return Generator
* This will yield strings, and return an int.
*/
function gen(): \Generator { ... }
The existing static analysis ecosystem already understands that syntax, and it would work equally well for generators used as context managers.
Should PHP adopt improved typing syntax for generators in the future (through generics or otherwise), context manager generators would inherit that new syntax automatically.
==== Value escape ====
It should be noted that there are ways for a context variable to "escape" from its ''using'' body. In particular, if some code in the block saves the value to some other property or a global, then it's behavior at block end may be unintended.
Specifically:
$holder = new class {
public File $f;
};
using (fileManager('file.txt') => $file) {
$holder->f = $file;
};
This will result in $file being unset at the end of the block, but not in $holder->f being unset. There are two possible ways to approach that issue.
- The ''fileManager'' Context Manager will close the file itself in a ''finally'' block. This will leave $holder->f as a reference to an already closed file.
- The ''fileManager'' does not close the file itself. $holder->f remains open, even though the ''using'' block has ended.
This is not a resolvable issue without something like Rust lifetimes. It is intrinsic in the way PHP memory management works. Which failure mode is less problematic will vary from one context to the next. Fortunately, the separation between Context Manager and Context Variable leaves it up to the implementer to determine which approach is most appropriate, or to even support both in different Context Managers or via a flag on the Context Manager. This flexibility is one of the key benefits of the Context Manager approach.
==== Differences from version 1 ====
The main difference from the previous version of the Context Manager RFC is the switch from a 2-method object to a generator to define the context manager. As discussed in the previous RFC, PHP's type handling is subtly different than Python's, making a very convenient idiom in Python not feasible in PHP. After several attempts, no generally acceptable workaround was discovered.
Switching to a Generator side-steps the issue entirely, at the cost of the type limitations described above. It also offers notably more flexibility than the 2-method approach, as it doesn't assume a decomposed try-catch model. As noted in the examples below, there are non-try-catch use cases for Context Managers as well that now become supported.
We had also struggled with the question of whether ''using'' should be an expression, as expressions are generally more flexible and useful but we couldn't figure out a good way to have it evaluate to a value (which all expressions must do). Using a generator, however, provides an obvious answer: Generators both ''yield'' and ''return'', allowing two channels to output data. One goes to the context variable, the other to the return value. With that, ''using'' being an expression became the obvious and natural choice.
If there is need, we are open to (in this RFC or a follow up) including an interface with a single generator method on it. See the Open Questions below.
==== Examples ====
The biggest value of context managers is their simplification of business logic code. In the examples below, compare the ''using'' code shown with implementing the same logic manually in dozens of places around a code base.
=== Database transactions ===
It's very common to use a closure to wrap a block of code in a database transaction, forcing it to either commit or rollback. However, doing so frequently requires manually re-''use''ing variables from the surrounding scope, which is frequently cumbersome. Context Managers offer a better alternative:
class DatabaseConnection
{
public function transaction(): self
{
try {
yield;
} catch (\Exception $e) {
$conn->rollback();
throw $e;
}
$conn->commit();
}
}
// Arbitrary place in code:
// Note that in this case context variable is omitted,
// as its return value is not needed.
using ($connection->transaction()) {
$connection->insert(blah blah);
// ...
};
=== Resource locking ===
As with database transactions, it's common to need to set aside a block of code that requires exclusive access to some resource, usually a file. Context managers make that easy. The example below wraps the core flock() function, but other implementations are possible.
/**
* @return \Generator
*/
function fileLock(string $file, bool $forWriting = true): \General
{
try {
$handle = fopen($file, $forWriting ? 'w' : 'r');
$locked = flock($handle, $tforWriting ? LOCK_EX : LOCK_SH);
if (!$locked) {
fclose($handle);
throw new \RuntimeException('Could not acquire lock.');
}
yield $handle;
} finally {
if ($locked) {
flock($handle, LOCK_UN);
}
fclose($handle);
}
}
// This code wants exclusive write access to this file:
using (fileLock('file.txt') => $fp) {
fwrite($fp, 'important stuff');
};
// Whereas this code doesn't want to write to the file,
// just use it for synchronization purposes.
using (fileLock('sentinel')) {
// Do stuff that doesn't involve a file at all.
};
=== Structured asynchronous control ===
Context managers also provide a convenient way to enforce structured asynchronous behavior. Assume for a moment the existence of a lower-level async library that has a class named ''Scope'', to which all coroutines are attached. Then the following code becomes possible. Note the use of the return value from the generator.
function blockingScope(): \Generator
{
$scope = new Scope();
try {
yield $scope;
foreach ($scope->routines as $r) {
$ret[] = $r->wait();
}
return $ret;
}
catch (\Exception $e) {
foreach ($scope->routines as $r) {
$r->cancel();
}
throw $e;
}
}
function cancelingScope(): \Generator
{
$scope = new Scope();
try {
yield $scope;
foreach ($scope->routines as $r) {
$r->cancel();
}
}
catch (\Exception $e) {
foreach ($scope->routines as $r) {
$r->cancel();
}
throw $e;
}
}
$results = using (blockingScope() => $s) {
$s->spawn(blah);
$s->spawn(beep);
$s->spawn(blargh);
};
// The code will block when it reaches this point, and not continue until all coroutines have completed.
// $results is an array of the results of the spawned coroutines.
using (cancellingScope() => $s) {
$s->spawn(blah);
$s->wait(5);
};
// Any coroutines still alive here gets canceled immediately. The code blocks until all coroutines complete their cancellation process.
Naturally, various other behavior such as integrated timeouts would be possible as well. Such an API could be provided by core in the future, or in user space.
=== Temporarily setting global configuration ===
There are times when a segment of code needs to modify some global runtime setting temporarily, such as disabling an error handler for example. That process is often fraught and error prone. That could be greatly simplified by wrapping it up into a context manager. A simplified example is shown below.
function customErrorHandler(\Closure $fn): \Generator
{
try {
$old = set_error_handler($fn);
yield;
} finally {
set_error_handler($old);
}
}
// Disable all error handling and live dangerously.
// Note that as there is no context variable returned, using
// the => clause is unnecessary.
using (customErrorHandler(fn() => null)) {
// Live dangerously here.
};
// We're guaranteed to have the previous error handler back by this point.
A similar class that allows changing an ini setting only within a given context is left as an exercise for the reader.
=== Timers ====
This example shows another use of the return value, and of a context manager that doesn't need try-catch.
function timer(): \Generator
{
$start = microtime(true);
yield;
$end = microtime(true);
return $end - $start;
}
$time = using (timer()) {
something_slow();
};
// Or alternatively, since this is a single-statement body:
$time = using(timer()) something_slow();
=== Suppressing selected exceptions ===
Python has a context manager called ''suppress'', which as its name implies lets the user list exceptions that should be ignored if thrown within a given context. While not useful in most cases, it would be very simple to implement.
function suppress(string ...$exceptions): \Generator
{
try {
yield;
} catch (\Exception $e) {
if (! array_any($this->ex, is_a($e, ?))) {
throw $e;
}
}
}
using (suppress(RecordNotFoundException::class)) {
// If it's not there, we don't care, the post-condition is the same.
$db->remove($id);
};
A inverted "allow list" version (suppress everything except listed exceptions) is left as an exercise for the reader.
Another example from Python is asserting the presence of an exception, for testing. In PHP, that could work very similar:
function expectException(string ...$exceptions): \Generator
{
try {
// Nothing interesting to do here.
yield;
}
catch (\Throwable $e) {
if (in_array($e::class, $this->es)) {
// Suppress just the specified exception.
return null;
}
}
throw new ExpectedExceptionNotThrownException();
}
class SomeTest extends TestCase {
static function assertContextThrows(string ...$exceptions) {
return new ExpectException(...$exceptions);
}
function testLoadThrowsWhenRecordNotFound() {
using (self::assertContextThrows(RecordNotFoundException::class)) {
// We expect this to throw. Test fails if it doesn't
$this->db->load($id);
};
}
}
===== Backward Incompatible Changes =====
A new semi-reserved keyword is introduced called ''using''. That means it will be disallowed for global constants or functions, but remain valid for existing methods or class constants.
===== Proposed PHP Version(s) =====
PHP 8.7
===== RFC Impact =====
==== To the Ecosystem ====
New syntax is introduced, which means SA tools will need to be updated and PHP FIG will likely need to issue a PER-CS update.
As a generic "setup and teardown" abstraction, future PHP APIs may be designed with this process in mind. That would allow avoiding extra syntax in favor of simply providing context managers for specific use cases. (The async example above is a key use case.)
==== To Existing Extensions ====
TBD.
==== To SAPIs ====
None.
===== Open Issues =====
==== Return values of multi-CM expressions ====
As noted above, should a multi-CM expression return just the outermost result, or an array of all?
==== Optional interface wrapper ====
In some cases, it may be ergonomically easier to use an Object as a context manager rather than a pure generator. We are open to adding the following interface to the RFC if there is interest:
interface ContextManager {
function getContextManager(): \Generator;
}
If an instance of ''ContextManager'' is found in a ''using'' block, the ''getContextManager()'' method will be called to produce the generator that will drive the rest of the interaction. This would simply be a convenience layer on top of the generator method, such that the following are logically equivalent:
using (new CM() => $val) {
// ....
};
using (new CM()->getContextManager() => $val) {
// ....
};
The authors have no strong feelings at this time, but are happy to include it as a convenience if there is interest.
===== Future Scope =====
==== send() support ====
Generators, in general, support a ''send()'' method for passing data back into them at the point of the ''yield''. It is not clear to us at this time that is useful to support, nor how it would be supported. However, we do not object to someone (now or in the future) finding a use case and syntax that would work for it, allowing the ''using'' body to pass a value back to the context manager.
===== Voting Choices =====
Pick a title that reflects the concrete choice people will vote on.
Please consult [[https://github.com/php/policies/blob/main/feature-proposals.rst#voting-phase|the php/policies repository]] for the current voting guidelines.
----
Primary Vote requiring a 2/3 majority to accept the RFC:
* Yes
* No
* Abstain
===== Implementation =====
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
===== References =====
Links to external references, discussions, or RFCs.
===== Rejected Features =====
Keep this updated with features that were discussed on the mail lists.
===== Changelog =====
If there are major changes to the initial proposal, please include a short summary with a date or a link to the mailing list announcement here, as not everyone has access to the wikis' version history.