PHP RFC: Scope Functions a.k.a. Multiline Short Closures

moebrowne1 pts0 comments

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...

scope function return functions baseurl token

Related Articles