PHP: rfc:scope-functions
rfc:scope-functions
PHP RFC: Scope Functions
Version: 1.0
Date: 2026-04-26
Authors: Bob Weinand bobwei9@hotmail.com, Volker Dusch edorian@php.net
Status: Under Discussion
Target: PHP 8.6
Implementation: Draft PR at https://github.com/php/php-src/pull/21968
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) {<br>// ...
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 share its parents’ scope makes these functions shorter and easier to read, write, and maintain.
$autoScoped = 1;<br>$closure = fn() {<br>var_dump($autoScoped);<br>$autoScoped = 2;<br>};
// int(1)<br>$closure();
// int(2)<br>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() {<br>$x = 1;<br>(fn() {<br>$x++;<br>$new = 'hi';<br>})();<br>// int(2), string(2) "hi"<br>var_dump($x, $new);
Beyond shared variables, a scope function behaves like any other closure:
Return: return returns 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, $this inside the closure is the same as outside of it.
extract(), compact(), $$var: All scope modifying functions and constructs to work and simply modify the parent's scope.
Examples
Async callbacks
// Current approach:<br>// - `use` for each callback<br>// - Results come back as an indexed array that the caller has to unpack.<br>function findSharedLikes($baseUrl, $userIdOne, $userIdTwo, $token, $client) {<br>$promises[] = \Amp\async(function () use ($baseUrl, $client, $userIdOne, $token) {<br>$req = $client->request("$baseUrl/api/user/$userIdOne/likes", ["Authorization" => "Bearer $token"]);<br>return $req->getBody()->buffer();<br>});<br>$promises[] = \Amp\async(function () use ($baseUrl, $client, $userIdTwo, $token) {<br>$req = $client->request("$baseUrl/api/user/$userIdTwo/likes", ["Authorization" => "Bearer $token"]);<br>return $req->getBody()->buffer();<br>});<br>$results = \Amp\await($promises);<br>return array_intersect($results[0], $results[1]);
// With scope functions:<br>// - results are written into nicely named variables in the parent scope<br>// - return is easy to read<br>function findSharedLikes($baseUrl, $userIdOne, $userIdTwo, $token, $client) {<br>$promises[] = \Amp\async(fn() {<br>$req = $client->request("$baseUrl/api/user/$userIdOne/likes", ["Authorization" => "Bearer $token"]);<br>$likesOne = $req->getBody()->buffer();<br>});<br>$promises[] = \Amp\async(fn() {<br>$req = $client->request("$baseUrl/api/user/$userIdTwo/likes", ["Authorization" => "Bearer $token"]);<br>$likesTwo = $req->getBody()->buffer();<br>});<br>\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<br>const TRANSACTION_ABORT = 1;
public function transaction(callable $callback): void {<br>try {<br>if ($callback() === self::TRANSACTION_ABORT) {<br>$this->rollback();<br>return;<br>} catch (\Throwable $e) {<br>$this->rollback();<br>throw $e;<br>$this->commit();
$connection->transaction(fn() {<br>// No need to capture $connection here or use $affectedRows by reference<br>$affectedRows = $connection->query("UPDATE ...");<br>if ($affectedRows === 0) {<br>return DatabaseConnection::TRANSACTION_ABORT;<br>// ...<br>});<br>// $affectedRows is available after the transaction<br>log("Ran transaction, updated $affectedRows rows");
Sorting with an external comparator
function sortByPriority(array $items, PriorityTable $priorities, Metrics $metrics): array {<br>// Track sort performance<br>$comparisons = 0;
// Today: $priorities could be used "normally" and $comparisons must be used by reference<br>usort($items, function ($a, $b) use ($priorities, &$comparisons) {<br>$comparisons++;<br>return $priorities->of($a) $priorities->of($b);<br>});
// With scope functions: Variables are simply in scope<br>usort($items, fn($a, $b) {<br>$comparisons++;<br>return $priorities->of($a) $priorities->of($b);<br>});
$metrics->recordComparisons($comparisons);<br>return $items;
Accumulation
Aggregation...