Skip to content

phptailors/context-interface

Repository files navigation

PHPUnit Composer Require Checker BC Check Psalm PHP CS Fixer

Context Manager Interfaces

A set of PHP interfaces that support implementation of Context Managers.

Introduction

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.

1. Specification

This specification defines four PHP interfaces. The first three formalize the core of the Context Managers concept. The fourth is a convenience interface.

1.1 Core Interfaces

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;
    }
}

1.2 Convenience Interfaces

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.

1.3 ExecutorFactory

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.

1.3.1 ExecutorFactory — the withContext() method

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.

1.4 Executor

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 */
            }
        );
    }
);

1.4.1 Executor — the do() method

An implementation of ExecutorInterface::do(), when invoked with a callable $func, MUST:

  1. invoke enterContext() on each Context Manager from $context in original order, and collect returned values, then
  2. invoke user-provided function $func passing it the values collected in 1 as arguments, and then
  3. invoke exitContext() on each Context Manager from Context in reverse order,
  4. return the value returned by $func.

If an exception is thrown during iteration 1:

  • the method MUST NOT invoke $func and 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 return null without 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;
    }
}

1.4 ContextManager

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.

1.4.1 ContextManager — the enterContext() method

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.

1.4.2 ContextManager — the exitContext() method

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).

2. Package

The interfaces described are provided as part of the phptailors/context-interface package.

3. ExecutorInterface

namespace Tailors\Lib\Context;

interface ExecutorInterface
{
    public function do(callable $func): mixed;
}

4. ExecutorFactoryInterface

namespace Tailors\Lib\Context;

interface ExecutorFactoryInterface
{
    public function withContext(ContextManagerInterface ...$context): ExecutorInterface;
}

5. ContextManagerInterface

namespace Tailors\Lib\Context;

interface ContextManagerInterface
{
    public function enterContext(): mixed;
    public function exitContext(?\Throwable $exception): ?\Throwable;
}

6. ExecutorServiceInterface

namespace Tailors\Lib\Context;
interface ExecutorServiceInterface
{
    public function with(mixed ...$args): ExecutorInterface;
}

8. References

About

Context Interface

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors