====== PHP RFC: Context Managers ======
* Version: 0.9
* Date: 2025-10-15
* Author: Larry Garfield (larry@garfieldtech.com), Arnaud Le Blanc (arnaud.lb@gmail.com)
* Status: In Discussion
* Implementation: https://github.com/arnaud-lb/php-src/pull/26
===== 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.
===== Proposal =====
This RFC introduces a new keyword, ''using'', and a corresponding interface, ''ContextManager''. They work together to create a "context block" in which certain behaviors are guaranteed to happen at the start, and at the end.
The interface is defined as follows:
interface ContextManager
{
public function enterContext(): mixed;
public function exitContext(?\Throwable $e = null): ?Throwable;
}
The syntax for ''using'' is as follows:
using ((EXPR [=> VAR])[,]+) {
BODY
}
Where:
* EXPR is any arbitrary expression that evaluates to an instance of ''ContextManager''.
* VAR is variable name that will be assigned the return value of ''enterContext()''. It may also be omitted if that value is not useful for a particular use case. This is known as the "context variable."
* BODY is any arbitrary collection of PHP statements.
The intent is to "factor out" the common setup and tear down processes from various use cases to the context manager object.
The EXPR could be ''new SomethingThatImplementsContextManager()'', or a static factory method call on such a class, or a function that returns such an object, or even a variable created previously that points to such an object.
A single ''using'' statement supports an arbitrary number of context manager clauses, though in most cases only a single one will be needed. (See the relevant section below.)
==== Behavior ====
At the start of the block, EXPR is validated to be a ''ContextManager'' instance. If not, a ''TypeError'' is thrown. Then ''enterContext()'' is called, which may take arbitrary setup steps and returns a value (which may be ''null'' if there is no useful context variable, or even ''$this'' to use the Context Manager as the context variable). If a context variable is specified, the return value will be assigned to that variable, otherwise it will be ignored. The statements within the block will execute normally.
The context variable will "mask" any existing variable of the same name. That is, if there is already a variable named ''$foo'', and the context variable is named ''$foo'', then inside the BODY ''$foo'' will be the context variable, and after the context block ends the original value of ''$foo'' will be restored. This masking applies only to the context variable, nothing else.
If the end of the block is reached, this is considered a success case. First, the ''exitContext()'' method is called with no arguments. It may take arbitrary cleanup steps, and its return value if any is ignored. Then, if VAR was set, it will be explicitly ''unset()'' and restored to its previous value, if any. If the context manager is not otherwise bound to a variable (as will typically be the case), it will go out of scope naturally at this point and be garbage collected like any other object.
If an exception is thrown in the course of the context block that propagates up to the context block, this is considered a failure case. First, the ''exitContext()'' method is called with the exception as its only parameter, which may take arbitrary cleanup steps. Then, if VAR was set, it will be explicitly ''unset()'' and restored to its previous value, if any. If the context manager is not otherwise bound to a variable (as will typically be the case), it will go out of scope naturally at this point and be garbage collected like any other object. If ''exitContext()'' returned ''null'', then no further action is taken. If ''exitContext()'' returns a throwable (either a new one or the one it was passed), it will be rethrown. ''exitContext()'' should NOT throw its own exceptions unless there is an error with the context manager object itself.
All exceptions or errors will be caught by the context manager. If ''exitContext()'' returns a throwable, it will be rethrown to any outer ''catch'' statements (or context manager blocks). If it returns null, then nothing is rethrown and execution will continue after the context block. Because in a success case the method is passed ''null'', that means always calling ''return $exception'' will result in the desired-in-most-cases behavior (that is, rethrowing an exception if there was one, or just continuing if not). Be aware that it should do so only after any appropriate local cleanup is done, such as closing files or IO connections.
There are three existing keywords that have special meaning within a ''using'' block (but not in functions called from the block).
$mgr = new SomeManager();
using ($mgr => $var) {
// Do stuff here, including referencing $mgr.
}
Alternatively, it is allowed for a context manager to return itself as the context variable. This is mainly useful for "resource replacement objects," where the object itself is what you want to use, but you want it to self-clean as well.
==== Implementation ====
The ''using'' block is desugared at compile time into more traditional code, and introduces no new control flow the the engine. This minimizes the impact on the engine, opcache, and optimizer. The best way to understand how the ''using'' block works is to see its desugared form.
using (new Manager() => $var) {
print "Hello world\n";
}
// Will compile into approximately the equivalent of this:
$__mgr = new Manager();
$__closed = false;
if (isset($var)) {
$__tmp = $var;
}
$var = $__mgr->enterContext();
try {
print "Hello world\n";
} catch (\Throwable $e) {
$__closed = true;
$__ret = $__mgr->exitContext($e);
if ($__ret instanceof Throwable) {
throw $e;
}
} finally {
if (!$__closed) {
$__mgr->exitContext();
}
unset($var);
unset($__closed);
unset($__mgr);
if (isset($__tmp)) {
$var = $__tmp;
unset($__tmp);
}
}
(Note the ''isset()'' checks that handle masking and restoring the context variable name.)
The
using (new Manager()) {
print "Hello world\n";
}
// Will compile into approximately the equivalent of this:
$__mgr = new Manager();
$__closed = false;
$__mgr->enterContext();
try {
print "Hello world\n";
} catch (\Throwable $e) {
$__closed = true;
$__ret = $__mgr->exitContext($e);
if ($__ret instanceof Throwable) {
throw $e;
}
} finally {
if (!$__closed) {
$__mgr->exitContext();
}
unset($__closed);
unset($__mgr);
}
==== Nested context managers ====
As noted above, a ''using'' statement may include multiple context manager expressions, separated by commas. The following is entirely legal syntax:
using (new Foo() => $foo) {
using (new Bar() => $bar) {
// Code that uses $foo and $bar here.
}
}
However, it is equivalent to this, more compact form:
using (new Foo() => $foo, new Bar() => $bar) {
// Code that uses $foo and $bar here.
}
When transpiling a multi-manager block, subsequent managers are "nested" inside the earlier ones. The later-defined managers will therefore be executed second, as though they were written separately. That is, the above will translate to approximately:
$m1 = new Foo();
$v1 = $m1->enterContext();
$thrown1 = false;
try {
$m2 = new Bar();
$v2 = $m2->enterContext();
$thrown2 = false;
try {
stmts
} catch (Throwable $e) {
$thrown2 = true;
$m2->exitContext($e);
} finally {
if (!$thrown2) {
$m2->exitContext();
}
unset($v2);
unset($m2);
}
} catch (Throwable $e) {
$thrown1 = true;
$m1->exitContext($e);
} finally {
if (!$thrown1) {
$m1->exitContext();
}
unset($v1);
unset($m1);
}
While in theory there is no fixed limit on how many context managers may be combined this way, in practice we expect it to be rare to have more than two, or three at the most.
==== Mixing with try-catch ====
In practice, we expect it be reasonably common that a ''using'' block 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 (new Manager() => $var) {
print "Hello world\n";
} catch(Exception $e) {
// ...
}
That will desugar to:
try {
using (new Manager() => $var) {
print "Hello world\n";
}
} catch(Exception $e) {
// ...
}
And the ''using'' statement will further desugar as already shown above. Multiple context managers are also supported the same way.
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 ''exitContext()'' not receiving an exception and thinking the process was successful. This way, we're guaranteed to always call ''exitContext()'', which in the typical case will rethrow for the one-off ''catch''.
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'' block with no desugaring, and can rethrow or not, as appropriate.
==== 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:
class ResourceContext implements ContextManager
{
public function __construct(private $resource) {}
public function enterContext(): mixed
{
return $this->resource;
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
close($this->resource);
return $e;
}
}
Which enables:
// This code
using (fopen('foo.txt', 'r') => $fp) {
fwrite($fp, 'bar');
}
// Will operate equivalent to this:
using (new 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.
Whenever resources are finally removed from the language, the file objects can implement ''ContextManager'' on their own and this special casing can be removed.
Naturally anyone can write their own context manager that wraps a resource and use that instead if the situation calls for it.
==== Exiting a block ====
This RFC presents the ''break'' keyword as a way to early-terminate a ''using'' block in a success case. That allows the code to skip straight to the cleanup phase (as defined by the Context Manager), without convoluted conditional structures.
For example, adding a record to a CSV file only if it's not already present could look like this:
$record = ['id' => 'larry', ...];
using(file_open('records.csv', 'rw') => $fp) {
while ($line = fgetcsv($fp) {
if ($line[0] === $record['id']) {
break 2; // Jumps to the end of the using block.
}
}
fputcsv($fp, $record);
}
// $fp is closed here, whether we wrote to it or not.
Writing this code without an early-termination option would be possible, but more convoluted and difficult to follow, especially as "early returns" are generally regarded as a good practice.
Strictly speaking, named labels and ''goto'' would also accomplish the goal. However, ''goto'' is generally discouraged, as it is less structured than ''break'' or ''continue''. Moreover, most other [[https://www.php.net/manual/en/control-structures.break.php|control structures respect ''break'']]. Making ''using'' the odd one out would serve no purpose but confusion.
==== Design notes ====
=== Keyword ===
Initially, the keyword used for context managers was ''with'', the same as Python. However, it was discovered during discussion that Laravel has a global function named ''with'' in one of its helper files, which would conflict with a new keyword. That means using ''with'' would render all Laravel installations incompatible with PHP 8.6 until Laravel made a backward-incompatible change itself. That is, of course, undesirable.
Separate surveys by Arnaud (focusing on just the semi-reserved case) and Seifeddine (covering all cases) found that ''using'' was virtually unused in the top 14000 packages on Packagist, appearing only two times (compared with 19 for ''with''). ''using'' is the keyword that C# uses for similar but less-robust functionality. Absent a better alternative, we have adopted ''using'' to minimize conflicts with existing code.
=== Return values and exception handling ===
In Python, the return value of %%__exit__()%% (equivalent to ''exitContext()'' here) is a boolean, with ''true'' indicating processing is done and any exception should not be further propagated, and ''false'' indicating the exception should be re-raised. In most cases, the method does not have a return statement, so evaluates to ''None'', which Python considers falsy. When there is no exception, this doesn't matter. The PHP equivalent would be a return type of
To facilitate chaining of contexts in Python code that directly manipulates context managers, %%__exit__()%% methods should not re-raise the error that is passed in to them. It is always the responsibility of the caller of the %%__exit__()%% method to do any reraising in that case. That way, if the caller needs to tell whether the %%__exit__()%% invocation failed (as opposed to successfully cleaning up before propagating the original error), it can do so. If %%__exit__()%% returns without an error, this can then be interpreted as success of the %%__exit__()%% method itself (regardless of whether or not the original error is to be propagated or suppressed). However, if %%__exit__()%% propagates an exception to its caller, this means that %%__exit__()%% itself has failed. Thus, %%__exit__()%% methods should avoid raising errors unless they have actually failed. (And allowing the original error to proceed isn’t a failure.)This same reasoning applies to PHP. There is value in distinguishing between a failure within the ''using'' block, and a failure of the context manager. A ''throw'' statement in the ''exitContext()'' method should be interpreted as a failure of the context manager. Given that every ''exitContext()'' method will necessarily have a ''return'' statement, we wanted to make that as self-evident as possible. A
$holder = new class {
public File $f;
};
using (new FileManager('file.txt') => $file) {
$holder->f = $file;
}
This will result in
class DatabaseTransaction implements ContextManager
{
public function __construct(
private DatabaseConnection $connection,
) {}
public function enterContext(): DatabaseConnection
{
return $this->connection;
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
if ($e) {
$this->connection->rollback();
} else {
$this->connection->commit();
}
return $e;
}
}
class DatabaseConnection
{
public function transaction(): DatabaseTransaction
{
return new DatabaseTransaction($this);
}
}
// Arbitrary place in code:
// Note that in this case the 'as' expression 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
class FileLock implements ContextManager
{
private $handle;
private bool $locked;
public function __construct(
private string $file,
private bool $forWriting = true,
) {}
public function enterContext(): mixed
{
$this->handle = fopen($this->file, $this->forWriting ? 'w' : 'r');
$this->locked = flock($this->handle, $this->forWriting ? LOCK_EX : LOCK_SH);
if (!$this->locked) {
throw new \RuntimeException('Could not acquire lock.');
}
return $this->handle;
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
if ($this->locked) {
flock($this->handle, LOCK_UN);
}
fclose($this->handle);
return $e;
}
}
// This code wants exclusive write access to this file:
using (new 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 (new 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:
class BlockingScope implements ContextManager
{
private Scope $scope;
public function enterContext()
{
return $this->scope = new Scope();
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
if ($e) {
foreach ($this->scope->routines as $r) {
$r->cancel();
}
}
else {
foreach ($this->scope->routines as $r) {
$r->wait();
}
}
return $e;
}
}
class CancellingScope implements ContextMananger
{
public function enterContext()
{
return $this->scope = new Scope();
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
foreach ($scope->routines as $r) {
$r->cancel();
}
return $e;
}
}
using (new BlockingScope() => $s) {
$s->spawn(blah);
}
// The code will block when it reaches this point, and not continue until all coroutines have completed.
using (new 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.
class CustomErrorHandler implements ContextManager
{
private $oldHandler;
public function __construct(
private $newHandler,
) {}
public function enterContext(): void
{
// There's no meaningful context variable here, so don't return anything.
$this->oldHandler = set_error_handler($this->newHandler);
}
public function exitContext(?\Throwable $e = null): ?Throwable
{
// The behavior is the same whether an exception was thrown or not.
set_error_handler($this->oldHandler);
return $e;
}
}
// 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.
===== Backward Incompatible Changes =====
A new global namespace interface is introduced called ''ContextManager''. It will conflict with any pre-existing symbols of that name. As the global namespace is generally considered reserved for PHP Internals, we do not anticipate any issues.
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.6.
===== 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 ====
The PoC implementation introduces 3 new opcodes:
* ''ZEND_INIT_USING'': Type checking ''EXPR'' and wrapping into ''ResourceManager'' if necessary
* ''ZEND_BACKUP_CV'': Backup the previous value of ''VAR'', if any
* ''ZEND_RESTORE_CV'': Restore the previous value of ''VAR'', if any
While these behaviors could have been achieved with desugaring, using these specialized opcodes leads to smaller code. It is unlikely to impact anyone, but extensions that manipulate opcodes directly for some reason may need to be aware of it.
==== To SAPIs ====
None.
===== Open Issues =====
===== Future Scope =====
==== Generator decorator managers ====
In Python, context managers may be implemented using generators. That allows using stand-alone functions and an annotation to write new context managers, rather than using an object.
We have verified that it is possible to write a generator-based wrapper context manager, which would then use a callable that contains its own try-catch blocks. That is, the following would be possible in user-space:
function opening($filename) {
return GeneratorDecorator::fromFactory(function () use ($filename) {
$f = fopen($filename, "r");
if (!$f) {
throw new Exception("fopen($filename) failed");
}
try {
yield $f;
} finally {
fclose($f);
}
});
}
using (opening(__FILE__) => $f) {
var_dump($f);
}
Where ''GeneratorDecorator'' is a standard boilerplate class ([[https://gist.github.com/arnaud-lb/71fe11c6abcc5e477510a45e2d7c1d01|code available]]). It would therefore be possible in the future to allow declaring a generator function/method to be a context manager, using an attribute to auto-wrap it. Something like:
#[ContextManager]
function opening($filename) {
$f = fopen($filename, "r");
if (!$f) {
throw new Exception("fopen($filename) failed");
}
try {
yield $f;
} finally {
fclose($f);
}
}
At this time, we don't feel it necessary to go this far. The object version gets the job done adequately. However, once we have more experience "in the wild" it's possible that such an approach would be a useful addition/shorthand. Such an addition would be straightforward to do, once its value was demonstrated.
===== Voting Choices =====
Main vote: 2/3 majority required to pass.