====== 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') as $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]].
===== 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): ?bool;
}
The syntax for ''using'' is as follows:
using ((EXPR [as 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 may return a value. If it does not return a value, ''null'' will be assumed. (This is true even if ''enterContext()'' is typed to return ''void''.) 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()''. 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()''. 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 ''true'', then no further action is taken. Otherwise, the exception will be rethrown without modification.
All exceptions or errors will be caught by the context manager. By default, the Throwable will be rethrown after the ''exitContext()'' method completes. To suppress the exception, ''exitContext()'' may return ''true''. Returning ''false'', ''null'', or not returning at all (which is the same as ''null'') will cause the Throwable to rethrow. Alternatively, the method may rethrow the same Throwable or a new Throwable (which should reference the original), in which case the original throw is also suppressed. 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).
break will jump to the end of the ''using'' block. This will be considered a success case as described above. If the ''break'' is within another control block, it will hit that first; use ''break 2'' or similar, as in other cases of nested control structures. This behavior is consistent with ''switch'' statements.
continue will behave the same as ''break'', but will trigger a warning. This is consistent with a switch statement, where ''continue'' within a ''case'' block triggers a warning. While not ideal, we felt it was best to keep the behavior consistent between ''using'' and ''switch'' rather than having two subtly different behaviors. Should ''continue'' in ''switch'' be cleaned up in the future (eg, to forbid it), the same cleanup should be done here. (''continue'' within a ''foreach'' or other control statement is fine.)
return will return from the function in which the ''using'' block resides, bypassing the remaining statements. This will trigger a success case as defined above, which will happen before the function returns.
Importantly, the ''using'' block does not create a new scope the way a function or closure does. Any variables that exist before the ''using'' block are available within it, and any variables created within the ''using'' block will continue to exist after it, with the exception of the context variable.
=== Context manager vs context variable ===
It is important to note that the Context Manager instance is //not// the same as the Context Variable. The Context Variable may be any legal PHP value. The Context Manager is a separate value that manages the lifetime of the Context Variable, and encapsulates relevant setup/teardown logic. Typically, the Context Manager object itself is never exposed to the body of the ''using'' statement. Additionally, it means that //user-space code should never call ''enterContext()'' and ''exitContext()'' itself//.
If it is desireable for some reason for the Context Manager to be available to the BODY, there are two ways of achieving that.
First, the Manager may be instantiated as a normal variable prior to the ''using'' statement and then simply referenced. It will then be available within the ''using'' block like any other variable.
$mgr = new SomeManager();
using ($mgr as $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. This minimizes the impact on the engine, opcache, and optimizer, as they will only see opcodes that already exist (plus some error handling, see below). The best way to understand how the ''using'' block works is to see its desugared form.
using (new Manager() as $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 !== true) {
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 $__mgr, $__closed, and $__ret variables will not actually be available by that name. They're just a convenient way to describe what happens.
A ''break'' statement in the body of the ''using'' block will be transformed into a ''goto'' to jump to the end of the ''using''. The transpilation step is smart enough to detect the level of the jump and whether it should be done. (Eg, a ''break'' inside ''using { foreach { } }'' should not be translated, but a ''break 2'' would be, etc.)
At runtime, opcodes equivalent to the above code is what will execute. Note that ''exitContext()'' is only ever called once. Any cleanup common to both success and failure cases should be handled appropriately.
Most of the above is implemented by generating the equivalent AST and compiling it. One exception is that type validation of the result of EXPR is implemented by a new opcode, for simplicity, and to reduce the size of generated code.
==== 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() as $foo) {
using (new Bar() as $bar) {
// Code that uses $foo and $bar here.
}
}
However, it is equivalent to this, more compact form:
using (new Foo() as $foo, new Bar() as $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 execited first, 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() as $var) {
print "Hello world\n";
} catch(Exception $e) {
// ...
}
That will desugar to:
try {
using (new Manager() as $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-dbs 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): ?bool
{
close($this->resource);
}
}
Which enables:
// This code
using (fopen('foo.txt', 'r') as $fp) {
fwrite($fp, 'bar');
}
// Will get translated into this:
using (new ResourceContext(fopen('foo.txt', 'r')) as $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 of the situation calls for it.
==== Design notes ====
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.
==== 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 DatabaseTransaction implements ContextManager
{
public function __construct(
private DatabaseConnection $connection,
) {}
public function enterContext(): DatabaseConnection
{
return $this->connection;
}
public function exitContext(?\Throwable $e = null): ?bool
{
if ($e) {
$this->connection->rollback();
} else {
$this->connection->commit();
}
}
}
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 flock() function, but other implementations are possible.
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): ?bool
{
if ($this->locked) {
flock($this->handle, LOCK_UN);
}
fclose($this->handle);
}
}
// This code wants exclusive write access to this file:
using (new FileLock('file.txt') as $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): ?bool
{
if ($e) {
foreach ($this->scope->routines as $r) {
$r->cancel();
}
}
else {
foreach ($this->scope->routines as $r) {
$r->wait();
}
}
}
}
class CancellingScope implements ContextMananger
{
public function enterContext()
{
return $this->scope = new Scope();
}
public function exitContext(?\Throwable $e = null): ?bool
{
foreach ($scope->routines as $r) {
$r->cancel();
}
}
}
using (new BlockingScope() as $s) {
$s->spawn(blah);
}
// The code will block when it reaches this point, and not continue until all coroutines have completed.
using (new CancellingScope() as $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): ?bool
{
// The behavior is the same whether an exception was thrown or not.
set_error_handler($this->oldHandler);
}
}
// Disable all error handling and live dangerously.
// Note that as there is no context variable returned, using
// the 'as' 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 ====
None.
==== To SAPIs ====
None.
===== Open Issues =====
==== Expression using ====
Should ''using'' be a statement, or an expression? Python's version is a statement, but there are potential advantages to an expression. In particular, it would allow it to be used in an expression context, such as ''match()''.
==== continue behavior ====
The behavior of ''continue'' described above is not ideal, but it is at least consistent with ''switch''. The alternative would be to disallow ''continue'' within ''using'' entirely right now. If ''switch'' is ever cleaned up, it would end up that way. If not, it would mean an inconsistency.
The challenge with ''switch'' is that removing support for ''continue'' could break existing code in exciting ways, as it would change all of the ''continue X'' statements present. It's therefore unclear if it will ever be feasible to address.
We are open to either approach for ''using'', depending on the consensus.
===== Future Scope =====
==== Generator decorator managers ====
In Python, context managers are implemented under the hood 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__) as $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 =====
* 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 =====
* [[https://externals.io/message/129077|Discussion Thread]]
* [[https://externals.io/message/129251|Bonus thread]]
===== Rejected Features =====
==== Destructor-based logic ====
Arguably, it would have been simpler to support any object as a context manager, and rely on its constructor as the "enter" operation and its destructor as the "exit" operation. However, that carries with it a number of problems:
- It would not allow a separation between the context manager and context variable. This is useful in many cases.
- Destructors do not take an argument, and therefore cannot differentiate between success and failure termination. That would make many cases, such as database transactions, impossible to implement.
- There is no way to reliably ensure that destructors are called as soon as the block exits. The context variable may still be referenced elsewhere when the block exits, thus preventing the destructor from being called. This can have negative impact on the program's reliability if the resource is expected to be closed in a timely manner. This may lead to resource exhaustion if the resource is referenced by a cycle, as the garbage collector is not guaranteed to execute soon enough.
- Destructors are called out of order (dependencies may be destroyed first) during garbage collection and shutdown.
For that reason, we did not pursue a destructor-based design.
===== Changelog =====
* 2025-11-13 - Change from ''with'' to ''using'', to avoid Laravel conflicts.
* 2025-11-13 - Add support for multiple context managers in one block.
* 2025-12-02 - Add masking for the context variable, if it already exists.
* 2025-12-03 - Add support for ''try using()'' desugaring.