This RFC proposes the addition of an inline syntax for lexical (captured) variables when declaring anonymous classes in PHP. The goal is to simplify anonymous class declarations and make them more concise by allowing developers to capture variables from the outer scope directly.
Anonymous classes were introduced in PHP 7.0, using a syntax that allows declaring a class definition and constructing an instance in a single statement. However, unlike anonymous functions, it is not easy to “capture” values from the parent scope for use inside the class; they can only be passed in via an explicit constructor or other method.
This RFC proposes a “property capture” syntax, where a property can be declared and initialised with $instance = new class use($captured) {};
Optional access modifiers, type, and renaming are also supported, e.g. $instance = new class use($localName as private readonly int $propertyName) {};
Capturing variables into an anonymous class instance requires two things:
Rather than inventing new syntax and semantics for these, this proposal reuses existing object properties and constructor parameters, as follows:
use
clause is present, generate an empty constructor which will be added to the anonymous class.use
clause, add it as a parameter to the constructor.use
clause (see below).public
and the default type to mixed
if not specified in the use
clause (see below).The implementation reuses a lot of mechanics from the existing Constructor Property Promotion mechanism, but this is considered an implementation detail, not a functional guarantee.
A new use
clause is introduced, immediately after the keywords “new class”, with the syntax use ([&]<var-name> as <modifiers> <type> <property-name>, ...)
. The <modifiers>
, <type>
, and <property-name>
are all optional; if none is specified, the “as” keyword must be omitted.
The simplest form of property capture resembles the capture list of anonymous functions:
$foo = 1; $bar = 2; $anon = new class use ($foo, $bar) {};
This declares a class with public untyped properties $foo
and $bar
, and creates an instance populating them from the outer variables $foo
and $bar
. In other words, it is equivalent to this:
$foo = 1; $bar = 2; $anon = new class($foo, $bar) { var $foo; var $bar; public function __construct($foo, $bar) { $this->foo = $foo; $this->bar = $bar; } };
By default, the property takes the same name as the outer variable, but this can be over-ridden using the syntax $varName as $propName
. This also allows the same local variable to be captured as the initial value for more than one property. For example:
$foo = 1; $bar = 2; $anon = new class use ($foo as $one, $bar as $two, $bar as $three) {};
Is equivalent to:
$foo = 1; $bar = 2; $anon = new class($foo, $bar, $bar) { var $one; var $two; vat $three; public function __construct($one, $two, $three) { $this->one = $one; $this->two = $two; $this->three = $three; } };
The as
keyword can also be used to modify the visibility and/or type of the declared property, either instead of or as well as renaming the property.
The modifiers allowed are the same as for Constructor Property Promotion, which is used internally to declare the properties; that is currently:
public
, protected
, or private
readonly
, which must be combined with a type specificationFor example:
$foo = 1; $bar = 2; $anon = new class use ($foo as private, $bar as protected readonly int, $bar as ?int $alsoBar) {};
Is equivalent to:
$foo = 1; $bar = 2; $anon = new class($foo, $bar, $bar) { private $foo; protected readonly int $bar; var ?int $alsoBar; public function __construct($foo, $bar, $alsoBar) { $this->foo = $foo; $this->bar = $bar; $this->alsoBar = $alsoBar; } };
It is possible to capture a variable by reference, by prefixing it with &
. For example:
$foo = 1; $anon = new class use (&$foo as $fooProp) {}; $foo = 2; echo $anon->fooProp;
Will print 2
. It is equivalent to:
$foo = 1; $anon = new class($foo) { var $one; public function __construct(&$one) { $this->one =& $one; } }; $foo = 2; echo $anon->fooProp;
TODO - expand
Create a struct-like object with readonly public properties:
$id = get_next_id(); $name = get_name(); $user = new readonly class use ($id, $name) {}; echo "{$user->id}: {$user->name}"; $user->id = 42; // ERROR: Cannot modify readonly property $id
Decorate a PSR-3 logger, adding some context to all entries logged:
use Psr\Log\{LoggerInterface,LoggerTrait}; function decorate_logger(LoggerInterface $logger, string $contextKey, mixed $contextValue): LoggerInterface { return new class use ($logger as private $innerLogger, $contextKey as private, $contextValue as private) implements LoggerInterface { public function log($level, string|\Stringable $message, array $context = []): void { $context[$this->contextKey] = $this->contextValue; $this->innerLogger->log($level, $message, $context); } }; }
The constructor, its parameters, and the properties, all appear as normal if the anonymous class is reflected, but can be detected with two new methods:
ReflectionParameter::isCaptured(): bool
ReflectionProperty::isCaptured(): bool
Although internally they are declared using Constructor Property Promotion, the parameters and properties return false
from ReflectionParameter::isPromoted
and ReflectionProperty::isPromoted
, as they are not written that way by the user.
The generated constructor itself is not marked, partly due to implementation concerns that a limited number of bits remain available in the fn_flags
bitmask.
The following new errors follow from the use of properties, rather than a new mechanism, to access the captured values:
new class use($foo, $foo) {}
or new class use($foo as $a, $bar as $a) {}
new class use($foo) { public $foo; }
The following new errors follow from the current implementation's use of a generated constructor:
new class use($foo) { public function __construct() {} }
new class($foo) use($bar) {}
Various alternatives to this restriction exist, discussed below. Throwing an Error now does not rule out any of these alternatives being implemented in future versions.
The restrictions on custom constructors can be worked around by adding a normal instance method, and calling it immediately.
That is, given a current definition like this:
$anon = new class($a, $b, $c) extends SomeOtherClass { private $a; public function __construct($a, $b, $c) { $this->a = $a; do_something($b); parent::__construct($c); } };
You could instead write this:
$anon = new class use($a) extends SomeOtherClass { public function init($b, $c) { do_something($b); parent::__construct($c); } }; $anon->init($b, $c);
It would be possible in principle to detect an existing constructor, and merge both its argument list and body with the generated code. That is, the above example could be written as:
# NOT supported in current proposal $anon = new class($b, $c) use($a) extends SomeOtherClass { public function __construct($b, $c) { do_something($b); parent::__construct($c); } };
The principle difficulty here is finding a point in the compilation process where the explicit constructor can be easily detected but still modified.
It also leads to additional error conditions, which would give confusing errors if not specifically checked for:
new class(...)
list, they would interact with the generated parameters for captured properties.
Another possibility is that any parameters passed to the new class
statement could be automatically passed to the parent constructor; so this:
# NOT supported in current proposal $anon = new class($foo) use($bar) extends SomeOtherClass {};
would be equivalent to this:
$anon = new class($foo, $bar) extends SomeOtherClass { var $bar; public function __construct($foo, $bar) { $this->bar = $bar; parent::__construct($foo); } };
This seems to avoid the need to look up and manipulate the existing constructor definition, but discovering a parent constructor is actually even more difficult, as inheritance is only resolved after compilation. That leads to a few difficulties:
$foo
to name the constructor parameter, but this could be any expression, e.g. $anon = new class(42 + some_function()) use ($bar) extends SomeOtherClass {};
. As such, the parameters would actually have to use generated names of some sort.Again, this leads to new error conditions which may be hard to understand to a user who doesn't know the details of the implementation.
It would also not be very consistent with the rest of the language, which neither generates nor requires calls to parent constructors.
Similar to the workaround above, the logic for setting captured property values could be generated in a method other than the constructor, e.g. __capture
.
That is, compile $anon = new class use ($foo) {};
to this:
$anon = new class { var $foo; public function __capture($foo) { $this->foo = $foo; } }; $anon->__capture($foo);
That would allow this:
# NOT supported in current proposal $anon = new class($b, $c) use($a) extends SomeOtherClass { public function __construct($b, $c) { do_something($b); parent::__construct($c); } };
To be equivalent to this:
$anon = new class($b, $c) extends SomeOtherClass { var $a; public function __construct($b, $c) { do_something($b); parent::__construct($c); } public function __capture($a) { $this->a = $a; } }; $anon->__capture($a);
The main complexity here is generating the additional method call - in the above example, it is shown as called on the local variable $anon
, but in practice, it could happen anywhere in an expression, e.g. some_function(new class($a) use($b) { ... });
.
A variation on the above would be to inject the extra method call (or the assignments themselves) immediately before the constructor is called.
Although it would give slightly nicer semantics, this would likely be even more challenging to implement, since the object creation and constructor call are both part of the NEW
opcode handler, so the additional logic would need to be added there, or in variant with a new opcode.
Another variation would be to have the constructor generated as in the current implementation, but call out to another magic method for the user to define additional constructor behaviour, effectively:
public function __construct($foo) { $this->foo = $foo; if ( method_exists($this, '__afterConstruct') ) { $this->__afterConstruct(); } }
If arguments are not supported, the advantage of this over existing workarounds is very slight. If they are supported, it would run into many of the same difficulties outlined in previous sections.
None. The new syntax is not currently valid PHP, and the behaviour of existing anonymous class declarations is unchanged.
Next PHP 8.x (hopefully 8.3)
Extensions manipulating the AST may encounter the new node kinds ZEND_AST_PROP_CAPTURE_LIST
and ZEND_AST_PROP_CAPTURE
, representing the list of properties and constructor parameters which are generated in the compiler.
None anticipated, but expert review on this point would be welcomed.
All existing features of anonymous classes are retained, and can be combined with the new use
clause, apart from the restrictions mentioned above. That includes:
__construct
readonly
When a renamed property is indicated with the $variable as $property
syntax, there is no technical need to name a local variable, rather than an arbitrary expression. In other words, it would be possible to allow this:
$anon = new class use (self::ID as $id, get_some_value() * 2 as private $something) {};
Which would be equivalent to this:
$anon = new class(self::ID, get_some_value() * 2) { public function __construct( public $id, private $something ) {} }
Taking the above a step further, the use
clause on anonymous functions could be extended in the same way, producing locally-scoped variables based on the captured values:
$callback = function() use (self::ID as $id, get_some_value() * 2 as $something) { do_something($id, $something); };
Add property capture to anonymous classes, with the syntax and semantics proposed, in PHP 8.3 (Yes / No, two-thirds majority required for acceptance)
After the project is implemented, this section should contain
Links to external references, discussions or RFCs