Context: Execution Contexts

Why This is Needed

There is an API with a service class that needs to perform actions tied to an authorization token. However, passing the token to every method of the service is a bad idea. In PHP, this problem is solved through global variables or static class properties. But in an asynchronous environment, where a single process can handle different requests, this approach won’t work, because at the time of the call, it’s unknown which request is being handled.

Async\Context allows storing data associated with a coroutine or Scope and building application logic based on the execution context.

What is Context

Async\Context is a key-value store bound to a Scope or coroutine. Contexts form a hierarchy: when reading a value, the search goes up the scope tree.

This is analogous to context.Context in Go or CoroutineContext in Kotlin. A mechanism for passing data through the hierarchy without explicitly passing parameters.

Three Levels of Context

TrueAsync provides three functions for accessing contexts:

<?php
use function Async\current_context;
use function Async\coroutine_context;
use function Async\root_context;

// Context of the current Scope
$scopeCtx = current_context();

// Context of the current coroutine
$coroCtx = coroutine_context();

// Global root context
$rootCtx = root_context();
?>

current_context()

Returns the context of the current Scope. If the context hasn’t been created yet, it creates one automatically. Values set here are visible to all coroutines in this Scope.

coroutine_context()

Returns the context of the current coroutine. This is a private context belonging only to this coroutine. Other coroutines cannot see data set here.

root_context()

Returns the global context, shared across the entire request. Values here are visible via find() from any context.

Keys

A key can be a string or an object:

<?php
use function Async\current_context;

$ctx = current_context();

// String key
$ctx->set('request_id', 'abc-123');

// Object as key (useful for unique tokens)
$key = new stdClass();
$ctx->set($key, 'value');
?>

Object keys are stored by reference in the context, which guarantees their uniqueness.

Reading: Local and Hierarchical

Searches for a value first in the current context, then in the parent, and so on up to the root:

<?php
use function Async\root_context;
use function Async\current_context;
use function Async\spawn;
use function Async\await;

root_context()->set('app_name', 'MyApp');

$scope = new Async\Scope();

spawn(function() {
    // find() searches up the hierarchy
    $name = current_context()->find('app_name');
    echo $name; // "MyApp" -- found in root_context
});
?>

findLocal() / getLocal() / hasLocal() – Current Context Only

Searches for a value only in the current context, without going up the hierarchy:

<?php
use function Async\root_context;
use function Async\current_context;

root_context()->set('app_name', 'MyApp');

$local = current_context()->findLocal('app_name');
// null -- this value is not set in the current Scope

$inherited = current_context()->find('app_name');
// "MyApp" -- found in parent scope
?>

Writing and Deleting

set()

<?php
$ctx = current_context();

// Set a value (default replace = false)
$ctx->set('key', 'value');

// Repeated set without replace -- error
$ctx->set('key', 'new_value'); // Error: A context key already exists

// With explicit replace = true
$ctx->set('key', 'new_value', replace: true); // OK

The set() method returns $this, allowing method chaining:

<?php
current_context()
    ->set('user_id', 42)
    ->set('request_id', 'abc-123')
    ->set('locale', 'en');
?>

unset()

<?php
$ctx = current_context();
$ctx->unset('key');

The unset() method also returns $this.

Practical Examples

Passing a Request ID

<?php
use function Async\spawn;
use function Async\await;
use function Async\current_context;

// Middleware sets the request_id
current_context()->set('request_id', bin2hex(random_bytes(8)));

// Any coroutine in this scope can read it
spawn(function() {
    $requestId = current_context()->find('request_id');
    // Use in logging
    error_log("[$requestId] Processing request...");
});
?>

Coroutine Context as Private Storage

<?php
use function Async\spawn;
use function Async\await;
use function Async\coroutine_context;

$c1 = spawn(function() {
    coroutine_context()->set('step', 1);
    // ... perform work
    $step = coroutine_context()->getLocal('step');
});

$c2 = spawn(function() {
    // Cannot see 'step' from c1
    $step = coroutine_context()->findLocal('step'); // null
});
?>

Configuration via root_context

<?php
use function Async\root_context;
use function Async\current_context;
use function Async\spawn;

// Set at the beginning of the request
root_context()
    ->set('db_host', 'localhost')
    ->set('cache_ttl', 3600);

// Available from any coroutine
spawn(function() {
    $dbHost = current_context()->find('db_host'); // "localhost"
});
?>

See Also