Inspiration: Scala
given/using(+ Kotlin coroutine context, React context, Lisp special variables) · Layer: lexer + parser + compiler + runtime
Cross-cutting values — a clock, a logger, a DB handle, the current request — usually
have to be threaded through every function signature by hand, even functions that
only pass them along. Context parameters let a value be provided once and flow
implicitly down the call chain to any function that declares it as a context
parameter, resolved by type.
class SystemClock { function time(): int { return time(); } }
function now(context Clock $c): int { return $c->time(); }
provide new SystemClock() { // active for everything called inside the block
echo now(); // $c is auto-supplied — no explicit argument
}A context parameter is never passed by the caller. The engine resolves it from the
nearest enclosing provide whose value matches the parameter's type (by class or
interface).
Block form — active for the dynamic extent of the block (everything it calls):
provide new SystemClock() {
handleRequest(); // any `context Clock` anywhere below here sees it
}Statement form — active for the rest of the enclosing function:
function handler(): void {
provide new RequestId('abc'); // active from here to the end of handler()
log('hi'); // a `context RequestId` deep below is found
}Both push onto a per-request context stack and pop automatically — the block on leaving the block, the statement when the function returns (or throws).
-
Nearest wins. A nested
provideof the same type shadows an outer one and is restored when it exits.provide new C('outer') { show(); // outer provide new C('inner') { show(); } // inner show(); // outer again }
-
Matched by type. Resolution walks the stack newest-first and returns the first value that is
instanceofthe declared type, so an interface or base class parameter is satisfied by any matching subtype:function now(context Clock $c): int { return $c->time(); } provide new SystemClock() { now(); } // SystemClock implements Clock ✓
-
Missing value is an error — unless the parameter has a default:
function need(context C $c): void {} need(); // Error: No context value of type C is available function withDefault(context C $c = new C()): void {} withDefault(); // uses the default; no provide needed
-
Always popped. A
provideblock that throws still pops its value, so context never leaks past it.
Dynamic-scoped context is the right tool for ambient dependencies that almost every
layer needs but almost no layer wants in its signature: the clock in tests, a
request-scoped logger, the current DB transaction, a correlation/request id. You get
the convenience of a global with the safety of lexical-looking scope — values are
visible only for the extent of a provide, and nested scopes compose.
A compile-time desugar plus a small runtime stack — no changes to the
parameter-binding / arg_info / RECV machinery (mirrors the #[Memoize] and
defer transforms):
-
contextparams → body prologue. Before a function is compiled, a pre-pass (zend_rewrite_context_paramsinZend/zend_compile.c) removes eachcontext T $cfrom the signature and prepends an initializer to the body:- no default:
$c = \__ctx_resolve("T"); - with default
D:$c = \__ctx_has("T") ? \__ctx_resolve("T") : (D);
So the public signature is a normal, context-free one — reflection, named arguments, and RECV numbering all behave as usual.
- no default:
-
provide→ push + scoped pop.- Block
provide V { B }→\__ctx_push(V); try { B } finally { \__ctx_pop(); }. - Statement
provide V;→\__ctx_push(V); defer \__ctx_pop();(reusing the existingdefer, so the pop runs at function exit on return and exception).
- Block
-
Runtime (
Zend/zend_context.c). A per-request stackEG(context_stack)holds the provided objects.__ctx_resolve/__ctx_haswalk it newest-first, matching withinstanceof_functionagainst the looked-up class;__ctx_resolvethrows if nothing matches.
Functions without context params and files without provide compile and run exactly
as before — the transform only fires where the keywords appear.
- Context values are objects, matched by class/interface. Scalars aren't context-resolvable (there's no type to match on).
- Context params on named functions, methods, and closures; not arrow functions (they have no block body).
- Auto-only: callers never pass a context parameter explicitly; there's no per-call override yet.
- The type is resolved by its written class name (non-namespaced / fully qualified in v1).
contextandprovideare case-sensitive, lowercase-only keywords — chosen so the very commonclass Context {}(capitalized) keeps working. They're also semi-reserved, so they remain usable as method/constant names ($o->context(...),$o->provide()).
smoke.sh— smoke teststests/context.phpt