====== PHP RFC: let construct (Block Scoping) ======
* Version: 1.2
* Date: 2025-09-08
* Author: Seifeddine Gmati azjezz@carthage.software, Tim Düsterhus timwolla@php.net
* Status: Under Discussion
* Implementation: https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
===== Introduction =====
This RFC proposes the introduction of the let() construct, a new language construct for managing lifetimes of variables. It allows developers to define a block or a statement in which one or more variables are in scope. Upon statement completion, these variables are automatically and reliably set back to their original value - or unset if previously undefined.
This construct addresses several recurring challenges in PHP. Often variables are only meant to be used in part of a function, for example after being checked by an if() statement or within the body of a loop. Accidentally reusing (temporary) variables after their intended purpose is a frequent source of subtle bugs, such as the classic foreach-by-reference issue, where the loop variable is reused and still attached to the last array element. Furthermore, developers often need to ensure that resources like file handles are properly disposed of, especially when gracefully handling exceptions for fallible operations. Manually managing this with verbose try-finally blocks or error-prone unset() calls clutters code and reduces clarity.
The let() construct provides a single, elegant solution to these common problems. By having an explicit construct to make scoping intent clear to human readers and static analysis tools alike, PHP can offer a safer and more readable way to write robust, maintainable code.
find(1)) if ($user !== null) {
printf("Hello %s!\n", $user->name);
}
// $user is now unset and accidentally using it can be reported by
// both PHP and static analysis tools.
assert(!isset($user));
===== Proposal =====
We propose a new let() construct. It takes a list of variable declarations, ensures they are defined, executes the associated statement list (block) and reliably sets the variables back to their original value (unsetting them if they were undefined) afterwards, no matter how the block is left. This guarantees that variables are unset immediately upon exiting the scope, rather than waiting for the function to end.
If a stored array or object is not referenced elsewhere (i.e. the reassignment causes the reference count to drop to zero), it will trigger the existing destruction logic, including calling the destructors of all objects that will (recursively) be freed.
This feature provides immense value, particularly for modern applications built on long-lived servers, where disciplined and immediate resource cleanup is not just a best practice, but a necessity for stability and performance.
By desugaring to a try-finally block internally, it provides a zero-cost abstraction that is both safe and highly ergonomic, eliminating the need for verbose manual cleanup. The try-finally block may be internally optimized out, if the engine detects that the only way to leave the let() block is by exiting the function, which will automatically unset() all variables.
For example, the following code:
let ($a = $b, $c) /* */
Is semantically equivalent to:
try {
if (array_key_exists('a', get_defined_vars())) {
$a_original = $a; // If the variable is undefined, no backup is made without emitting a warning.
unset($a); // Break references.
}
$a = $b;
if (array_key_exists('c', get_defined_vars())) {
$c_original = $c;
unset($c);
} else {
$c = null; // If $c was not declared, it's initialized to null.
}
/* */
} finally {
if (array_key_exists('c_original', get_defined_vars())) {
$c = $c_original; // If a backup was made, it is restored.
} else {
unset($c); // Otherwise the variable is unset.
}
if (array_key_exists('a_original', get_defined_vars())) {
$a = $a_original;
} else {
unset($a);
}
}
The full syntax is defined as follows:
::= "let" "(" ")"
::= { "," }
::= ( "=" ) |
==== Constraints ====
* Neither global nor static variables may be defined within the statement list.
* static variables may not be be a scoped variable.
* goto into the statement list is disallowed.
==== Examples ====
Simple example:
Example showing an edge case (solving the foreach reference bug):
int(2) [1]=> int(4) [2]=> int(6) }
Example showing the combination of let and if():
name);
}
// $user will no longer be in scope, preventing accidental use outside
// of the if() checking whether or not the User could be found.
Example showing reliable resource management:
lock(LockType::Shared),
) {
// The lock ensures that the entire file contents are
// read in an atomic fashion and are internally consistent,
// but no longer needs to be held once we obtained the
// entire contents and begin to process them.
$content = $file->readAll();
}
// The file lock is released here, immediately after it is no longer needed,
// which is crucial in high-concurrency environments.
} catch (Exception $e) {
// The file lock will also be released before handling the Exception.
sleep(10); // Simulate expensive exception handling.
throw new Exception('Processing failed', previous: $e);
}
sleep(10); // Simulate expensive processing
return $content;
}
Example showing error situations (Error messages are not final / not part of the actual proposal and may change as necessary to clarify):
==== Design Choices ====
Variables in PHP exist implicitly in the local (function) scope without needing to be declared. We believe it is a strength of PHP that avoids boilerplate code for simple functions and thus improves developer experience. The proposed let construct is therefore built to be fully opt-in with straightforward semantics that make it easy to reason about variable lifetimes, particularly when mixing function-scoped and block-scoped variables.
Therefore it specifically requires block-scoped variables to be declared at the start of a block to avoid ambiguity and rules that need to be learned by heart for cases like the following:
Line 5 is making use of autovivification of arrays, where an array is created when setting the first array item on an undefined variable. This is something that is fully supported by PHP without emitting any Deprecations, Notices, Warnings, or Errors. With the variable declaration in line 8, it is not intuitively clear whether or not the access to $array in line 5 is legal, whether it refers to a function-scoped or block-scoped version of $array and what the value of $array will be in line 13. Similar considerations apply for scope introspection functionality like get_defined_vars(), compact(), or variable variables. Some of these may be frowned upon in modern PHP, but are nevertheless a non-deprecated part of PHP.
Other languages with block scoping, particularly statically typed languages, avoid this ambiguity by requiring all variables to be explicitly declared, making variable declarations and reasoning about lifetimes an integral part of the development process and making correct usage easy to check at compile time.
JavaScript, which added block-scoping in more recent versions of the language, already required the use of variable declarations for variables to be declared in the function scope rather than referring to the global scope. Thus developers of JavaScript are already familiar with needing to declare variables and the more explicit semantics around lifetimes.
By allowing both statements and statement lists as the “body” of the let() construct, it composes nicely with if() statements to “set and check” variables without introducing additional nesting. Block-scoping the loop variables for foreach() loops to avoid specifying them twice is part of the future scope of this RFC that doesn't compromise on “variables need to be declared at the start of the scope”.
===== Backward Incompatible Changes =====
let will become a semi-reserved keyword and will no longer be available as a function name. This affects 4 usages within the top 18,975 composer packages containing a total of 626,044 PHP files.
===== Proposed PHP Version(s) =====
Next PHP 8.x
===== RFC Impact =====
==== To the Ecosystem ====
* IDEs, LSPs, and Static Analyzers: Will need updates to understand the new scoping rules. This will enable them to provide correct autocompletion and error analysis, correctly identifying when a variable is out of scope.
* Auto-Formatters and Linters: Will require updates to support the new syntax.
==== To Existing Extensions ====
* The current implementation contains two new OPcodes (ZEND_BACKUP_SCOPE, ZEND_RESTORE_SCOPE) that extensions working on OPcodes might need to be aware of.
==== To SAPIs ====
None.
===== Open Issues =====
None.
===== Future Scope =====
This RFC lays the foundation for explicit resource management in PHP. Future proposals could build upon it
* Adding foreach ($foo as let $bar) syntactic sugar for let ($bar) foreach ($foo as $bar)
* Adding a attribute / marker interface to automatically wrap resource objects into a WeakReference when capturing a backtrace.
* Throwing errors when marked resource objects have RC > 1 at the end of the block.
* Introducing a Disposable interface (similar to C#'s IDisposable) to allow objects to define custom, explicit cleanup logic that is automatically called by use.
* Extending the let construct to support other resource management patterns.
===== Voting Choices =====
Primary Vote requiring a 2/3 majority to accept the RFC:
* Yes
* No
* Abstain
===== Patches and Tests =====
A proof-of-concept implementation is available at:
https://github.com/php/php-src/compare/master...TimWolla:php-src:block-scope
===== Implementation =====
After the RFC is implemented, this section should contain:
* The version(s) it was merged into
* A link to the git commit(s)
* A link to the PHP manual entry for the feature
===== References =====
* [[https://docs.python.org/3/reference/compound_stmts.html#the-with-statement|Python’s with statement]]
* [[https://docs.hhvm.com/hack/statements/using|Hack’s using statement]]
* [[https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/using|C#’s using statement]]
* RFC Discussion thread: https://news-web.php.net/php.internals/129059
===== Rejected Features =====
None.
===== Changelog =====
* 2025-11-23: Add Design Choices section. Explain that the try-finally may be optimized away. Note that the current implementation defines two new OPcodes.
* 2025-11-18: Use let() as the keyword.
* 2025-11-12: Add list of constraints
* 2025-11-12: Clarify that references are broken upon entering the scope.
* 2025-11-05: The original values will now be restored instead of unconditionally unsetting everything.