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
Last revisionBoth sides next revision
rfc:property-capture [2023/04/22 20:50] imsoprfc:property-capture [2023/04/23 21:14] imsop
Line 1: Line 1:
 ====== PHP RFC: Property Capture for Anonymous Classes ====== ====== PHP RFC: Property Capture for Anonymous Classes ======
-  * Version: 0.2 +  * Version: 0.3 
-  * Date: 2023-04-22+  * Date: 2023-04-23
   * 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 24: Line 24:
 Rather than inventing new syntax and semantics for these, this proposal reuses existing object properties and constructor parameters, as follows: 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. +  - 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. +  - 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). +  - 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).+  - 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.   - 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.   - When creating an //instance// of the anonymous class, pass the variables listed as parameters to the constructor.
Line 35: Line 35:
 ===== 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 152: Line 152:
 echo $anon->fooProp; echo $anon->fooProp;
 </code> </code>
 +
 +===== Examples =====
 +
 +**TODO - expand**
 +
 +Create a struct-like object with readonly public properties:
 +
 +<code php>
 +$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
 +</code>
 +
 +Decorate a [[https://www.php-fig.org/psr/psr-3/|PSR-3]] logger, adding some context to all entries logged:
 +
 +<code php>
 +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] = $contextValue;
 +            $this->innerLogger->log($level, $message, $context);
 +        }
 +   };
 +}
 +</code>
 +
 +===== 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 ===== ===== Restrictions =====
  
-Because it generates both property declarations and constructor, the new syntax has a few restrictionswhich are indicated with new Errors thrown by the compiler:+The following new errors follow from the use of properties, rather than a new mechanismto access the captured values:
  
   * "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>   * "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>   * "Captured property $a conflicts with existing property", e.g. <php>new class use($foo) { public $foo; }</php>
 +
 +The following new errors follow from the current implementation's use of a generated constructor:
 +
   * "Cannot declare custom constructor for anonymous class with captured properties", e.g. <php>new class use($foo) { public function __construct() {} }</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>   * "Cannot pass constructor arguments to anonymous class with captured properties", e.g. <php>new class($foo) use($bar) {}</php>
  
-==== Workarounds ====+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. 
 + 
 +==== Current Workaround ====
  
 The restrictions on custom constructors can be worked around by adding a normal instance method, and calling it immediately. The restrictions on custom constructors can be worked around by adding a normal instance method, and calling it immediately.
Line 196: Line 244:
  
 <code php> <code php>
-# NOT supported in proposal+# NOT supported in current proposal
 $anon = new class($b, $c) use($a) extends SomeOtherClass { $anon = new class($b, $c) use($a) extends SomeOtherClass {
     public function __construct($b, $c) {     public function __construct($b, $c) {
Line 214: Line 262:
 ==== Alternative 2: Automatically Calling Parent Constructor ==== ==== 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:+Another possibility is that any parameters passed to the ''new class'' statement could be automatically passed to the parent constructor; so this:
  
 <code php> <code php>
-# NOT supported in proposal+# NOT supported in current proposal
 $anon = new class($foo) use($bar) extends SomeOtherClass {}; $anon = new class($foo) use($bar) extends SomeOtherClass {};
 </code> </code>
Line 225: Line 273:
 <code php> <code php>
 $anon = new class($foo, $bar) extends SomeOtherClass { $anon = new class($foo, $bar) extends SomeOtherClass {
-    private $bar;+    var $bar;
     public function __construct($foo, $bar) {     public function __construct($foo, $bar) {
         $this->bar = $bar;         $this->bar = $bar;
Line 244: Line 292:
 ==== Alternative 3: Generating a Different Method ==== ==== Alternative 3: Generating a Different Method ====
  
-==== Alternative 4: Calling an Additional Magic 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%%''
  
-===== Reflection =====+That is, compile <php>$anon new class use ($foo) {};</php> to this:
  
-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:+<code php> 
 +$anon = new class { 
 +    var $foo; 
 +    public function __capture($foo) { 
 +        $this->foo = $foo; 
 +    } 
 +}; 
 +$anon->__capture($foo); 
 +</code>
  
-  * ''ReflectionParameter::isCaptured(): bool'' +That would allow this:
-  * ''ReflectionProperty::isCaptured(): bool''+
  
-Although internally they are declared using Constructor Property Promotionthe parameters and properties return ''false'' from ''ReflectionParameter::isPromoted'' and ''ReflectionProperty::isPromoted'', as they are not written that way by the user.+<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 generated constructor itself is not markedpartly due to implementation concerns that a limited number of bits remain available in the ''fn_flags'' bitmask.+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 3b: Initialising Before the Constructor Call ==== 
 + 
 +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 semanticsthis 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. 
 + 
 +==== 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 =====
Line 273: Line 373:
 None anticipated, but expert review on this point would be welcomed. None anticipated, but expert review on this point would be welcomed.
  
 +===== Unaffected Functionality =====
 +
 +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:
 +
 +  * Inheriting parent classes
 +  * Implementing interfaces
 +  * Using traits
 +  * Implementing any method other than ''%%__construct%%''
 +  * Declaring the entire class ''readonly''
  
 ===== Future Scope ===== ===== Future Scope =====
Line 278: Line 387:
 ==== Arbitrary Expressions ==== ==== Arbitrary Expressions ====
  
-===== Proposed Voting Choices ===== +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 expressionIn other words, it would be possible to allow this:
-Include these so readers know where you are heading and can discuss the proposed voting options.+
  
-===== Patches and Tests ===== +<code php> 
-Links to any external patches and tests go here.+$anon new class use (self::ID as $id, get_some_value() * 2 as private $something) {}; 
 +</code>
  
-If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed.+Which would be equivalent to this:
  
-Make it clear if the patch is intended to be the final patchor is just a prototype.+<code php> 
 +$anon = new class(self::IDget_some_value() * 2) { 
 +    public function __construct( 
 +        public $id, 
 +        private $something 
 +    ) {} 
 +
 +</code>
  
-For changes affecting the core language, you should also provide patch for the language specification.+==== Extension to Anonymous Functions ==== 
 + 
 +Taking the above step further, the ''use'' clause on anonymous functions could be extended in the same way, producing locally-scoped variables based on the captured values: 
 + 
 +<code php> 
 +$callback = function() use (self::ID as $id, get_some_value() * 2 as $something) { 
 +    do_something($id, $something); 
 +}; 
 +</code> 
 + 
 +===== Proposed Voting Choices ===== 
 + 
 +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 ===== 
 + 
 +https://github.com/php/php-src/pull/11123
  
 ===== Implementation ===== ===== Implementation =====
Line 301: Line 433:
  
 ===== 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