PHP RFC: Scope Functions
- Version: 0.2
- Date: 2026-04-26
- Authors: Bob Weinand bobwei9@hotmail.com, Volker Dusch edorian@php.net
- Status: Draft
- Target: PHP 8.6
- Implementation: Draft at https://github.com/php/php-src/compare/master...bwoebi:php-src:scope_fn
Introduction
This RFC proposes a variant of closures for the common case of a callback that runs inside its defining function.
Current PHP closures that are supposed to affect outside variables require explicit capturing:
function () use (&$x, &$y, &$z) { // ... }
The list is verbose, potentially unfamiliar to readers due to references, and silently wrong when forgotten as adding a new variable to the body without updating use() just shadows the variable.
For use cases involving the array_* functions, transaction wrappers, async callbacks, sorting, and others, having closure automatically its parents’ scope makes these functions shorter and easier to read, write, and maintain.
$autoScoped = 1; $closure = fn () { var_dump($autoScoped); $autoScoped = 2; } // int(1) $closure(); // int(2) var_dump($autoScoped);
Proposal
Introduce a new type of closure that shares variables with the enclosing scope.
Syntax
fn(parameter_list)[: return_type] { body }
Disambiguated from arrow functions by a { block vs the => expression. The fn keyword is reused to avoid introducing a new keyword. fn(...) { ... }.
Semantics
A scope function is a Closure whose variables are scoped to the parent function. All reads, writes, and newly introduced variables share the parent's scope:
function example() { $x = 1; (fn() { $x++; $new = 'hi'; })(); // int(2), string(2) "hi" var_dump($x, $new); }
Beyond shared variables, a scope function behaves like any other closure:
- Return:
returnreturns from the scope function only, not from the parent. - Exceptions: Exceptions propagate normally to the caller of the scope function, then up the stack.
- Call stack: Calls to a scope function appear as their own frame in
debug_backtrace()and stack traces. $this: Inside of methods,$thisinside the closure is the same as outside of it.extract(), compact(), $$var: All scope modifing functions and constructs to work and modify the parent's scope.
Examples
Async callbacks
// Current approach: // - `use` for each callback // - Results come back as an indexed array that the caller has to unpack. function findSharedLikes($baseUrl, $userIdOne, $userIdTwo, $token, $client) { $promises[] = \Amp\async(function () use ($baseUrl, $client, $userIdOne, $token) { $req = $client->request("$baseUrl/api/user/$userIdOne/likes", ["Authorization" => "Bearer $token"]); return $req->getBody()->buffer(); }); $promises[] = \Amp\async(function () use ($baseUrl, $client, $userIdTwo, $token) { $req = $client->request("$baseUrl/api/user/$userIdTwo/likes", ["Authorization" => "Bearer $token"]); return $req->getBody()->buffer(); }); $results = \Amp\await($promises); return array_intersect($results[0], $results[1]); } // With scope functions: // - results are written into nicely named variables in the parent scope // - return is easy to read function findSharedLikes($baseUrl, $userIdOne, $userIdTwo, $token, $client) { $promises[] = \Amp\async(fn() { $req = $client->request("$baseUrl/api/user/$userIdOne/likes", ["Authorization" => "Bearer $token"]); $likesOne = $req->getBody()->buffer(); }); $promises[] = \Amp\async(fn() { $req = $client->request("$baseUrl/api/user/$userIdTwo/likes", ["Authorization" => "Bearer $token"]); $likesTwo = $req->getBody()->buffer(); }); \Amp\await($promises); return array_intersect($likesOne, $likesTwo); }
Transaction wrapper
A simple callback-based DB transaction wrapper that can be used with exceptions or return values.
class DatabaseConnection { const TRANSACTION_ABORT = 1; public function transaction(callable $callback): void { try { if ($callback() === self::TRANSACTION_ABORT) { $this->rollback(); return; } } catch (\Throwable $e) { $this->rollback(); throw $e; } $this->commit(); } } $connection->transaction(fn () { // No need to capture $connection here or use $affectedRows by reference $affectedRows = $connection->query("UPDATE ..."); if ($affectedRows === 0) { return DatabaseConnection::TRANSACTION_ABORT; } // ... }); // $affectedRows is available after the transaction log("Ran transaction, update affected $affectedRows rows");
Sorting with an external comparator
function sortByPriority(array $items, PriorityTable $priorities, Metrics $metrics): array { // Track sort performance $comparisons = 0; // Today: $priorities could be used "normally" and $comparisons must be used by reference usort($items, function ($a, $b) use ($priorities, &$comparisons) { $comparisons++; return $priorities->of($a) <=> $priorities->of($b); }); // With scope functions: Variables are simply in scope usort($items, fn($a, $b) { $comparisons++; return $priorities->of($a) <=> $priorities->of($b); }); $metrics->recordComparisons($comparisons); return $items; }
Accumulation
Aggregation of numbers into a total and count to avoid floating points errors in averages.
function aggregate(array $numbers): array { $count = '0'; $sum = '0'; // Currently: Both accumulators must be listed in use() with & array_walk(function ($number) use (&$count, &$sum) { $count = bcadd($count, '1'); $sum = bcadd($sum, $number); }, $numbers); // With scope functions: $count and $sum are just used. array_walk(fn($number) { $count = bcadd($count, '1') $sum = bcadd($sum, $number) )}, $numbers); return [$count, $sum]; }
Filtering with a side channel for errors
function validateList(Validator $validator, array $items): array { $errors = []; // Today: $validator (read) and $errors (write) both have to be listed. $valid = array_filter($items, function ($item) use ($validator, &$errors) { if (!$reason = $validator->isValid($item)) { $errors[] = [$item, $reason]; return false; } return true; }); // With scope functions: nothing to list, intent reads top to bottom. $valid = array_filter($items, fn($item) { if (!$reason = $validator->isValid($item)) { $errors[] = [$item, $reason]; return false; } return true; }); return [$valid, $errors]; }
Restrictions
A scope function cannot leave the lifetime of its defining function.
The following forms are therefore not useful and thus forbidden:
static fn() {}:$thisis already shared, a static scope function would be incoherent.
Fatal error: Scope functions cannot be static in %s on line %d
fn() use () {}: Every variable is already shared.use()is redundant.
Parse error: syntax error, unexpected token "use", expecting "{" in %s on line %d
- Recursion: Recursive calls will raise an
Error.
Fatal error: Uncaught Error: Cannot recursively call scope function in %s:%d
- Cloning:
clone $fnon a scope function raises anError. Two clones would share the same parent scope, interfering with each other's state.
- Rebinding:
Closure::bind()andClosure::bindTo()raise anError. The scope and$thiscome from the parent and cannot be rebound.
- Outliving the parent: A scope function may only be called while its defining function is still on the stack. Storing one in a property, a global, a static variable, or returning it from the parent raises an
Errorwhen the parent returns.
Fatal error: Uncaught Error: Scope function closure must not outlive the declaring scope in %s:%d
- Re-evaluation in the same frame: Re-evaluating the same scope function declaration in the same parent frame invalidates the previous instance rather than silently retargeting it to the new one.
Fatal error: Uncaught Error: Cannot call scope function: defining scope has exited in %s:%d
This avoids silently retargeting an old closure handle to a new instance.
- Generators: There are no restrictions on yield/yield from. Generators will be closed as the scoped function goes out of scope.
Alternatives Considered
Letting an older closure handle transparently resolve to the current instance when the same scope function declaration is re-evaluated:
This was rejected because it makes closure identity unstable: A stored closure would silently change meaning after later execution of the parent frame.
Reflection
ReflectionFunctionAbstract::isScopeFunction(): bool
Returns true for scope function closures. Available on ReflectionFunction.
Backward Incompatible Changes
None. fn(...) { ... } is currently a parse error.
Open Issues
None.
Future Scope
Scoped functions can be inlined with their callers. For example, for array_map this is a trivial operation that will improve performance.
Proposed PHP Version
PHP 8.6.
Voting
Accept the scope functions feature as described? Yes/No. Requires 2/3 majority.