A set of PHP interfaces that support implementation of Context Managers.
The concept of Context Managers is borrowed from Python (contextlib, with statement) and PHP RFC: Context Managers.
Context Managers provide a way to abstract out common control-flow and variable-lifetime-management patterns, leading to simplified business logic code that can skip over a great deal of boilerplate. They place certain guards around a section of code, such that certain behaviors are guaranteed upon entering and leaving that section.
Commmon use cases include file management and database transactions, althrough there are many more.
PHP 8 does not provide Context Managers natively, but there is a proposal and a pull request for the feature, so it may appear in future versions. As for the time of this writting (March, 2026), the RFC has status "In Discussion".
This specification defines PHP interfaces, which may be used to implement Context Managers in PHP 8. An implementation will be referred to as a library through the rest of this document.
This specification defines four PHP interfaces. The first three formalize the core of the Context Managers concept. The fourth is a convenience interface.
The three core interfaces are
With these three interfaces implemented, an application shall be able to use the following syntax to execute a function with Context Managers in action:
/** @var ExecutorFactoryInterface $executor */
$executor->withContext([C1[, C2[, ...]]])->do(
function ([$arg1,[ $arg2[, ...]]]) {
/* code */
}
);The C1, C2, ... are expressions, each of which MUST evaluate to
an instance of ContextManagerInterface.
Using the above syntax, a cannonical example — a code for robust file handling that includes all necessary error management and automatic cleanup — could be written as follows:
/** @var ExecutorFactoryInterface $executor */
$line = $executor->withContext(new FileContextManager(fopen("foo.txt", "rt")))->do(
function (mixed $handle): string {
return fgets($handle);
}
);with FileContextManger being an implementation of
ContextManagerInterface, such as
readonly class FileContextManager implements ContextMangerInterface {
private mixed $handle;
public function __construct(mixed $handle) {
$this->handle = $handle;
}
public function enterContext(): mixed {
return $this->handle;
}
public function exitContext(?\Throwable $exception): ?\Throwable {
fclose($this->handle);
return $exception;
}
}The convenience interfaces include
The ExecutorServiceInterface is a more abstract version of the ExecutorFactoryInterface. It reduces the boilerplate required for wrapping Context Values with ContextManagers. The syntax introduced by ExecutorServiceInterface is
/** @var ExecutorServiceInterface $executor */
$executor->with([V1[, V2[, ...]]])->do(
function ([$arg1[, $arg2[, ...]]]) {
/* code */
}
);The V1, V2, ... are expressions, each of which evaluates to an
arbitrary value, called a Context Value. It's the responsibility of
ExecutorService to wrap the provided Context Values
with appropriate ContextManagers.
With the above syntax, the cannonical example (robust file handling) would look like follows:
/** @var ExecutorServiceInterface $executor */
$line = $executor->with(fopen("foo.txt", "rb"))->do(
function (mixed $handle): string {
return fgets($handle);
}
);The difference is, that there is no call to new FileContextManager(...) in
the above snippet.
An implementing class for the ExecutorFactoryInterface will be called ExecutorFactory across this document, although implementors may chose a different name. A minimal implementation of the ExecutorFactoryInterface may look like:
use Taylors\Context\ExecutorFactoryInterface;
use Taylors\Context\ExecutorInterface;
final class ExecutorFactory implements ExecutorFactoryInterface {
public function withContext(ContextManagerInterface ...$context): ExecutorInterface {
return new Executor($context);
}
}provided there is a class Executor that implements the ExecutorInterface:
The purpose of the ExecutorFactory is to expose the withContext() method, which is an entry point similar to the using keyword from the PHP RFC.
The withContext() method of ExecutorFactory returns an
Executor which adds the behavior specified by Context Managers to
the execution of a user-provided callback. The returned Executor
MUST have access to the ContextManagers passed as
...$context to the withContext() method.
An implementing class of the ExecutorInterface will be called Executor across this document, although implementors may chose a different name.
The Executor is coupled with a $context — an ordered
collection of zero or more ContextManagers. The
$context is supposed to be (created out of) the array of
...$context arguments passed to ExecutorFactory::withContext().
It must preserve element order and keys of ...$context.
In a nutshell, the Executor invokes a user-provided callback
$func within Executor's $context — it calls
enterContext() on all the ContextManagers
from $context, then executes the user-provided callback $func, and
then calls exitContext() on all the
ContextManagers in $context.
An implementation must ensure, that using Executor with multiple
ContextManagers is equivalent to nesting
Executors with consecutive ContextManagers
from the same ...$context, i.e. the effect of
/** @var ExecutorFactoryInterface */
$executor->withContext(E1, E2)->do(
function ($arg1, $arg2) {
/* code */
}
);is same as for
/** @var ExecutorFactoryInterface */
$executor->withContext(E1)->do(
function ($arg1) use ($executor) {
return $executor->withContext(E2)->do(
function ($arg2) use ($arg1) {
/* code */
}
);
}
);An implementation of ExecutorInterface::do(), when invoked with a callable $func, MUST:
- invoke enterContext() on each Context
Manager from
$contextin original order, and collect returned values, then - invoke user-provided function
$funcpassing it the values collected in 1 as arguments, and then - invoke exitContext() on each Context Manager from Context in reverse order,
- return the value returned by
$func.
If an exception is thrown during iteration 1:
- the method MUST NOT invoke
$funcand MUST proceed to 3.
In addition, if an exception is thrown during iteration 1 or from $func
- the reverse iteration in 3 MUST start from the last ContextManager successfully visited in 1,
- if any of the ContextManagers visited in 3 returns
null, the Executor MUST returnnullwithout throwing, - otherwise, if all ContextManagers returned
\Throwable, the method MUST rethrow the exception returned by last visited ContextManager.
If any of the ContextManagers returned null during
the reverse iteration 3, the exception is assumed handled by that
ContextManager and is not passed to the remaining
ContextManagers.
An example implementation of Executor::do() may look like follows:
class Executor implements ExecutorInterface {
/**
* @var array<ContextManagerInterface>
*/
private array $context;
/* ... */
public function do(callable $func): mixed
{
$exception = null;
$return = null;
$entered = [];
try {
$args = [];
foreach ($this->context as $name => $manager) {
$args[$name] = $manager->enterContext();
$entered[] = $manager;
}
$return = call_user_func_array($func, $args);
} catch(\Throwable $e) {
$exception = $e;
}
while (count($entered) > 0) {
$manager = array_pop($entered);
if (null === $exception) {
$manager->exitContext();
} else {
$exception = $manager->exitContext($exception);
}
}
return $return;
}
}An implementing class of the ContextManagerInterface will be called ContextManager across this document, although implementors may chose different names.
A ContextManager object implements enterContext() and exitContext() for a single Context Value. In a typical scenario, multiple classes implementing ContextManageInterface will be provided by a library to handle e.g. different value types.
Enter the runtime context related to this object. The returned value will be passed by Executor to an appropriate argument of the user-provide callback.
In most circumstances, the method returns the wrapped Context Value or a value derived from it.
Exit the runtime context related to this object.
For a successful case, the exitContext() is called with no arguments. It may take arbitrary cleanup steps, and its return value if any is ignored.
If an exception is thrown in the course of the context block that propagates up
to the context block, this is considered a failure. The
exitContext() method is called with the exception as its only
parameter. If exitContext() returns null, then no further
action is taken. If exitContext() returns a \Throwable
(either the one it was passed or a new one), it will be rethrown.
exitContext() should NOT throw its own exceptions unless there
is an error with the context manager object itself.
Becausu 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).
The interfaces described are provided as part of the phptailors/context-interface package.
namespace Tailors\Lib\Context;
interface ExecutorInterface
{
public function do(callable $func): mixed;
}namespace Tailors\Lib\Context;
interface ExecutorFactoryInterface
{
public function withContext(ContextManagerInterface ...$context): ExecutorInterface;
}namespace Tailors\Lib\Context;
interface ContextManagerInterface
{
public function enterContext(): mixed;
public function exitContext(?\Throwable $exception): ?\Throwable;
}namespace Tailors\Lib\Context;
interface ExecutorServiceInterface
{
public function with(mixed ...$args): ExecutorInterface;
}