rfc:property-capture

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Next revisionBoth sides next revision
rfc:property-capture [2023/04/15 20:51] imsoprfc:property-capture [2023/04/22 21:16] imsop
Line 1: Line 1:
-====== PHP RFC: Your Title Here ====== +====== PHP RFC: Property Capture for Anonymous Classes ====== 
-  * Version: 0.1 +  * Version: 0.2 
-  * Date: 2023-04-15+  * Date: 2023-04-22
   * Author: Rowan Tommins (imsop@php.net)   * Author: Rowan Tommins (imsop@php.net)
   * Thanks: Nicolas Grekas (nicolasgrekas@php.net), Ilija Tovilo (tovilo.ilija@gmail.com)   * Thanks: Nicolas Grekas (nicolasgrekas@php.net), Ilija Tovilo (tovilo.ilija@gmail.com)
Line 8: Line 8:
  
 ===== Introduction ===== ===== Introduction =====
 +
 +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. 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 <php>$instance = new class use($captured) {};</php> Optional access modifiers, type, and renaming are also supported, e.g. <php>$instance = new class use($localName as private readonly int $propertyName) {};</php> This RFC proposes a "property capture" syntax, where a property can be declared and initialised with <php>$instance = new class use($captured) {};</php> Optional access modifiers, type, and renaming are also supported, e.g. <php>$instance = new class use($localName as private readonly int $propertyName) {};</php>
 +
 +===== Semantics =====
 +
 +Capturing variables into an anonymous class instance requires two things:
 +
 +  - A way to refer to the captured variables inside the //class definition//, which is compiled once, when the statement is first run
 +  - A way to associate values with these variables for //a particular instance//, which will vary each time the statement is run
 +
 +Rather than inventing new syntax and semantics for these, this proposal reuses existing object properties and constructor parameters, as follows:
 +
 +  - If a ''use'' clause is present, generate an empty constructor which will be added to the anonymous class.
 +  - For each captured variable in the ''use'' clause, add it as a parameter to the constructor.
 +  - Declare a property with the same name as the captured variable in the anonymous class, unless renamed in the ''use'' clause (see below).
 +  - Set the default visibility of the property to ''public'' and the default type to ''mixed'' if not specified in the ''use'' clause (see below).
 +  - In the constructor body, assign the captured variable to the corresponding property.
 +  - When creating an //instance// of the anonymous class, pass the variables listed as parameters to the constructor.
 +
 +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.
  
 ===== Syntax ===== ===== Syntax =====
  
-A new "use clauseis 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.+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.
  
 ==== Basic Form ==== ==== Basic Form ====
Line 107: Line 127:
 </code> </code>
  
 +==== Capture by Reference ====
  
-===== Restrictions and Errors =====+It is possible to capture a variable by reference, by prefixing it with <php>&</php>. For example: 
 + 
 +<code php> 
 +$foo 1; 
 +$anon new class use (&$foo as $fooProp) {}; 
 +$foo 2; 
 +echo $anon->fooProp; 
 +</code> 
 + 
 +Will print <php>2</php>. It is equivalent to: 
 + 
 +<code php> 
 +$foo 1; 
 +$anon new class($foo) { 
 +    var $one; 
 +     
 +    public function __construct(&$one) { 
 +        $this->one =& $one; 
 +    } 
 +}; 
 +$foo 2; 
 +echo $anon->fooProp; 
 +</code> 
 + 
 +===== Examples ===== 
 + 
 +**TODO**
  
 ===== Reflection ===== ===== Reflection =====
 +
 +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.
 +
 +===== Restrictions =====
 +
 +Because it generates both property declarations and a constructor, the new syntax has a few restrictions, which are indicated with new Errors thrown by the compiler:
 +
 +  * "Redefinition of captured property", e.g. <php>new class use($foo, $foo) {}</php> or <php>new class use($foo as $a, $bar as $a) {}</php>
 +  * "Captured property $a conflicts with existing property", e.g. <php>new class use($foo) { public $foo; }</php>
 +  * "Cannot declare custom constructor for anonymous class with captured properties", e.g. <php>new class use($foo) { public function __construct() {} }</php>
 +  * "Cannot pass constructor arguments to anonymous class with captured properties", e.g. <php>new class($foo) use($bar) {}</php>
 +
 +==== Workarounds ====
 +
 +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:
 +
 +<code php>
 +$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);
 +    }
 +};
 +</code>
 +
 +You could instead write this:
 +
 +<code php>
 +$anon = new class use($a) extends SomeOtherClass {
 +    public function init($b, $c) {
 +        do_something($b);
 +        parent::__construct($c);
 +    }
 +};
 +$anon->init($b, $c);
 +</code>
 +
 +==== Alternative 1: Merging Constructors ====
 +
 +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:
 +
 +<code php>
 +# 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);
 +    }
 +};
 +</code>
 +
 +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:
 +
 +  * If too few or too many parameters are passed in the ''new class(...)'' list, they would interact with the generated parameters for captured properties.
 +  * If the captured properties are added to the end of the parameter list, constructor parameters with default values would be disallowed.
 +
 +==== Alternative 2: Automatically Calling Parent Constructor ====
 +
 +Another suggestion is that any parameters passed to the ''new class'' statement could be automatically passed to the parent constructor; so this:
 +
 +<code php>
 +# NOT supported in current proposal
 +$anon = new class($foo) use($bar) extends SomeOtherClass {};
 +</code>
 +
 +would be equivalent to this:
 +
 +<code php>
 +$anon = new class($foo, $bar) extends SomeOtherClass {
 +    var $bar;
 +    public function __construct($foo, $bar) {
 +        $this->bar = $bar;
 +        parent::__construct($foo);
 +    }
 +};
 +</code>
 +
 +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:
 +
 +  * It would be possible for the parent class to have a constructor with an incompatible signature, or no constructor at all
 +  * The above example uses the input ''$foo'' to name the constructor parameter, but this could be any expression, e.g. <php>$anon = new class(42 + some_function()) use ($bar) extends SomeOtherClass {};</php>. 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.
 +
 +==== Alternative 3: Generating a Different Method ====
 +
 +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 <php>$anon = new class use ($foo) {};</php> to this:
 +
 +<code php>
 +$anon = new class {
 +    var $foo;
 +    public function __capture($foo) {
 +        $this->foo = $foo;
 +    }
 +};
 +$anon->__capture($foo);
 +</code>
 +
 +That would allow this:
 +
 +<code php>
 +# 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);
 +    }
 +};
 +</code>
 +
 +To be equivalent to this:
 +
 +<code php>
 +$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);
 +</code>
 +
 +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. <php>some_function(new class($a) use($b) { ... });</php>.
 +
 +==== Alternative 4: Calling an Additional Magic Method ====
 +
 +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:
 +
 +<code php>
 +public function __construct($foo) {
 +    $this->foo = $foo;
 +    if ( method_exists($this, '__afterConstruct') ) {
 +        $this->__afterConstruct();
 +    }
 +}
 +</code>
 +
 +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.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
-What breaks, and what is the justification for it?+ 
 +None. The new syntax is not currently valid PHP, and the behaviour of existing anonymous class declarations is unchanged.
  
 ===== Proposed PHP Version(s) ===== ===== Proposed PHP Version(s) =====
Line 120: Line 327:
  
 ===== RFC Impact ===== ===== RFC Impact =====
-==== To SAPIs ==== 
-Describe the impact to CLI, Development web server, embedded PHP etc. 
- 
 ==== To Existing Extensions ==== ==== To Existing Extensions ====
-Will existing extensions be affected?+ 
 +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.
  
 ==== To Opcache ==== ==== To Opcache ====
-It is necessary to develop RFC's with opcache in mindsince opcache is a core extension distributed with PHP.+None anticipatedbut expert review on this point would be welcomed.
  
-Please explain how you have verified your RFC's compatibility with opcache.+===== Unaffected Functionality =====
  
-==== New Constants ==== +All existing features of anonymous classes are retained, and can be combined with the new ''use'' clause, apart from the restrictions mentioned aboveThat includes:
-Describe any new constants so they can be accurately and comprehensively explained in the PHP documentation.+
  
-==== php.ini Defaults ==== +  * Inheriting parent classes 
-If there are any php.ini settings then list: +  * Implementing interfaces 
-  * hardcoded default values +  * Using traits 
-  * php.ini-development values +  * Implementing any method other than ''%%__construct%%'' 
-  * php.ini-production values+  * Declaring the entire class ''readonly''
  
-===== Open Issues ===== +===== Future Scope =====
-Make sure there are no open issues when the vote starts!+
  
-===== Unaffected PHP Functionality ===== +==== Arbitrary Expressions ====
-List existing areas/features of PHP that will not be changed by the RFC.+
  
-This helps avoid any ambiguity, shows that you have thought deeply about the RFC's impact, and helps reduces mail list noise.+**TODO**
  
-===== Future Scope =====+==== Extension to Anonymous Functions ====
  
-==== Constructor Support ==== +**TODO**
- +
-==== Arbitrary Expressions ====+
  
 ===== Proposed Voting Choices ===== ===== Proposed Voting Choices =====
-Include these so readers know where you are heading and can discuss the proposed voting options.+ 
 +Add property capture to anonymous classes, with the syntax and semantics proposed, in PHP 8.3 (Yes / No, two-thirds majority required for acceptance)
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
-Links to any external patches and tests go here. 
  
-If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed. +**TODO**
- +
-Make it clear if the patch is intended to be the final patch, or is just a prototype. +
- +
-For changes affecting the core language, you should also provide a patch for the language specification.+
  
 ===== Implementation ===== ===== Implementation =====
Line 177: Line 373:
  
 ===== Rejected Features ===== ===== Rejected Features =====
-Keep this updated with features that were discussed on the mail lists.+
rfc/property-capture.txt · Last modified: 2023/04/23 21:15 by imsop