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);
Introduce a new type of closure that shares variables with the enclosing scope.
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(...) { ... }.
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 returns from the scope function only, not from the parent.debug_backtrace() and stack traces.$this: Inside of methods, $this inside 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.// 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); }
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");
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; }
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]; }
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]; }
A scope function cannot leave the lifetime of its defining function.
The following forms are therefore not useful and thus forbidden:
static fn() {}: $this is 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
Error.Fatal error: Uncaught Error: Cannot recursively call scope function in %s:%d
clone $fn on a scope function raises an Error. Two clones would share the same parent scope, interfering with each other's state.Closure::bind() and Closure::bindTo() raise an Error. The scope and $this come from the parent and cannot be rebound.Error when the parent returns.Fatal error: Uncaught Error: Scope function closure must not outlive the declaring scope in %s:%d
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.
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.
ReflectionFunctionAbstract::isScopeFunction(): bool
Returns true for scope function closures. Available on ReflectionFunction.
None. fn(...) { ... } is currently a parse error.
None.
Scoped functions can be inlined with their callers. For example, for array_map this is a trivial operation that will improve performance.
PHP 8.6.
Accept the scope functions feature as described? Yes/No. Requires 2/3 majority.