====== PHP RFC: Scope Functions ====== * Version: 0.2 * Date: 2026-04-26 * Authors: Bob Weinand , Volker Dusch * 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:** ''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 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() {}'':** ''$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 * **Recursion:** Recursive calls will raise an ''Error''. Fatal error: Uncaught Error: Cannot recursively call scope function in %s:%d * **Cloning:** ''clone $fn'' on a scope function raises an ''Error''. Two clones would share the same parent scope, interfering with each other's state. * **Rebinding:** ''Closure::bind()'' and ''Closure::bindTo()'' raise an ''Error''. The scope and ''$this'' come 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 ''Error'' when 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. ===== References ===== * [[https://wiki.php.net/rfc/arrow_functions_v2|Arrow Functions 2.0 RFC]] * [[https://wiki.php.net/rfc/closures|Original Closures RFC]] * [[https://wiki.php.net/rfc/closure-optimizations|Closure optimizations]]