rfc:property_accessors

Differences

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

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
rfc:property_accessors [2021/05/03 17:08]
nikic
rfc:property_accessors [2022/04/28 09:30] (current)
ilutov Fix typos
Line 3: Line 3:
   * Author: Nikita Popov <nikic@php.net>   * Author: Nikita Popov <nikic@php.net>
   * Proposed Version: PHP 8.1   * Proposed Version: PHP 8.1
-  * Implementation: https://github.com/php/php-src/pull/6873 (WIP) +  * Implementation: https://github.com/php/php-src/pull/6873 
-  * Status: Draft+  * Status: Under Discussion
  
 ===== Introduction ===== ===== Introduction =====
Line 363: Line 363:
 </PHP> </PHP>
  
-Specifying the same accessor multiple types is also illegal.+Specifying the same accessor multiple times is also illegal.
  
 === By-reference getter === === By-reference getter ===
Line 387: Line 387:
 </PHP> </PHP>
  
-These indirect modification perform a call to the getter, followed by a call to the setter.+These indirect modifications perform a call to the getter, followed by a call to the setter.
  
 However, indirect array modification, as well as acquiring a reference are only supported by by-reference getters. In this case, the setter will not be invoked: However, indirect array modification, as well as acquiring a reference are only supported by by-reference getters. In this case, the setter will not be invoked:
Line 420: Line 420:
  
 While nominally well-defined, such an accessor would not have particularly useful semantics: It would be possible to change the property value arbitrarily, but only by writing ''$ref =& $test->prop; $ref = $value'' rather than ''$test->prop = $value''. While nominally well-defined, such an accessor would not have particularly useful semantics: It would be possible to change the property value arbitrarily, but only by writing ''$ref =& $test->prop; $ref = $value'' rather than ''$test->prop = $value''.
 +
 +TODO: An open problem is how implicit by-value ''get'' interacts with by-reference foreach. Currently, it's still possible to acquire a reference using this pathway:
 +
 +<PHP>
 +class Test {
 +    public $prop = null { get; private set; }
 +}
 +
 +$test = new Test;
 +foreach ($test as &$prop) {
 +    $prop = 1;
 +}
 +</PHP>
 +
 +This needs to be prevented in some way. This could either error, or it could silently return a value for properties for which no reference can be acquired. Otherwise it would no longer be possible to iterate arbitrary objects by-reference.
  
 === Isset and unset === === Isset and unset ===
Line 496: Line 511:
 var_dump($test->prop); // Calls Test::$prop::get() var_dump($test->prop); // Calls Test::$prop::get()
 </PHP> </PHP>
 +
 +=== Interaction with by-reference getter ===
 +
 +It is worth pointing out that the combination of ''&get'' with ''private set'' leaves a loophole that allows you to bypass the private setter and perform arbitrary modifications:
 +
 +<PHP>
 +class Test {
 +    public array $prop = [] { &get; private set; }
 +}
 +$test = new Test;
 +$test->prop[] = 42; // Allowed!
 +$ref =& $test->prop;
 +$ref = [1, 2, 3]; // Allowed!
 +</PHP>
 +
 +It would be possible to make this do something more meaningful by making the "by-reference" part of the ''get'' accessor use the visibility of the ''set'' accessor instead.
  
 ==== Inheritance ==== ==== Inheritance ====
Line 508: Line 539:
 class A { class A {
     public $prop {     public $prop {
-        get { echo __METHOD, "\n";+        get { echo __METHOD__, "\n";
-        set { echo __METHOD, "\n"; }+        set { echo __METHOD__, "\n"; }
     }     }
 } }
 class B extends A { class B extends A {
     public $prop {     public $prop {
-        set { echo __METHOD, "\n"; }+        set { echo __METHOD__, "\n"; }
     }     }
 } }
Line 534: Line 565:
     public int|string $covariant { get; }     public int|string $covariant { get; }
     // This property is useless, but will serve for the sake of illustration.     // This property is useless, but will serve for the sake of illustration.
-    public int|string $contravariant { set {} }+    public int|string $contravariant { set { /* ... */ } }
 } }
  
Line 634: Line 665:
 <PHP> <PHP>
 class A { class A {
-    // This is legal also for non-accessor properties. +    final public $prop1; 
-    final public $prop { get; }+    final public $prop2 { get; }
 } }
  
 class B extends A { class B extends A {
-    // Illegal, property is final. +    // Illegal, properties are final. 
-    public $prop { set; }+    public $prop1 { get; set; } 
 +    public $prop2 { set; }
 } }
 </PHP> </PHP>
Line 757: Line 789:
  
 This matches the current behavior of an incompatible (non-accessor) property in a used trait and a using class. I believe the current behavior is a bug and should be addressed generally. For now, this proposal is "bug-compatible". This matches the current behavior of an incompatible (non-accessor) property in a used trait and a using class. I believe the current behavior is a bug and should be addressed generally. For now, this proposal is "bug-compatible".
 +
 +=== Private accessor shadowing ===
 +
 +Private accessors can be shadowed by accessors in child classes. In line with usual behavior, accessing the property from the class where the private accessor is defined, will use the private accessor rather than a public shadower in a child class:
 +
 +<PHP>
 +class A {
 +    public $prop {
 +        get { echo __METHOD__, "\n"; }
 +        private set { echo __METHOD__, "\n"; }
 +    }
 +
 +    public function test() {
 +        $this->prop;
 +        $this->prop = 1;
 +    }
 +}
 +
 +class B extends A {
 +    public $prop {
 +        get { echo __METHOD__, "\n"; }
 +        set { echo __METHOD__, "\n"; }
 +    }
 +}
 +
 +$a = new A;
 +$a->test();
 +$b = new B;
 +$b->test();
 +
 +// Prints:
 +// A::$prop::get
 +// A::$prop::set
 +// B::$prop::get
 +// A::$prop::set
 +</PHP>
 +
 +When accessed on an instance of ''B'' from scope ''A'', reading the property calls ''B::$prop::get()'', as the original accessor was public and has been overridden. However, writing the property calls ''A::$prop::set()'', as the original accessor was private, and is only shadowed in the child class.
 +
 +This should also extend to the case where the child class replaces the accessor property with an ordinary property:
 +
 +<PHP>
 +class A {
 +    public $prop {
 +        get { echo __METHOD__, "\n"; }
 +        private set { echo __METHOD__, "\n"; }
 +    }
 +
 +    public function test() {
 +        $this->prop;
 +        $this->prop = 1; // Should always call A::$prop::set()
 +    }
 +}
 +
 +class B extends A {
 +    public $prop;
 +}
 +</PHP>
 +
 +TODO: The current implementation does not handle this case correctly.
  
 === TODO: Parent accessors === === TODO: Parent accessors ===
Line 764: Line 856:
 This syntax can't extend to other accessor types though, so those would either have no way to invoke the parent accessor, or invoke it implicitly. For possible ''guard'' accessors, it would make sense to automatically chain the accessors (though the order is not entirely obvious). For ''lazy'' accessors this wouldn't be possible though. This syntax can't extend to other accessor types though, so those would either have no way to invoke the parent accessor, or invoke it implicitly. For possible ''guard'' accessors, it would make sense to automatically chain the accessors (though the order is not entirely obvious). For ''lazy'' accessors this wouldn't be possible though.
  
-Other syntax choices like ''parent::$prop::get()'' are somewhat ambiguous, for example this choice looks like a static property access followed by a static method call.+Other syntax choices like ''parent::$prop::get()'' are somewhat ambiguous, for example this syntax looks like a static property access followed by a static method call.
  
 In any case, adding support for parent accessors will be technically non-trivial, because we need to perform a modified-scope property access through a separate syntax. In any case, adding support for parent accessors will be technically non-trivial, because we need to perform a modified-scope property access through a separate syntax.
Line 807: Line 899:
 </PHP> </PHP>
  
-However, it is possible to specify only a ''get'' accessor. In this case, the property still only allows one initializing assignment, and becomes read-only subsequently:+However, it is possible to specify only a ''get'' accessor. In this case, the property only allows one initializing assignment, and becomes read-only subsequently:
  
 <PHP> <PHP>
Line 828: Line 920:
 class Test { class Test {
     // Illegal: Implicit get, explicit set.     // Illegal: Implicit get, explicit set.
-    public string $prop { get; set {} }+    public string $prop { 
 +        get; 
 +        set { /* ... */ } 
 +    }
     // Illegal: Implicit set, explicit get.     // Illegal: Implicit set, explicit get.
-    public string $prop { get {} set; }+    public string $prop { 
 +        get { /* ... */ } 
 +        set; 
 +    }
 } }
 </PHP> </PHP>
Line 853: Line 951:
     // Illegal: Default value on property with explicit accessors.     // Illegal: Default value on property with explicit accessors.
     public $prop = "" {     public $prop = "" {
-        get {}+        get { /* ... */ }
     }     }
 } }
Line 886: Line 984:
 This limitation exists to prevent embedding of very large property declarations in the constructor signature. This limitation exists to prevent embedding of very large property declarations in the constructor signature.
  
-=== var_dump, get_object_vars() etc ===+=== var_dump(), array cast, foreach etc ===
  
-''var_dump()'', and other functions or language features inspecting object properties will only return properties that have a backing property, i.e. those using implicit accessors. Explicit accessor properties are omitted, and accessors will never be evaluated as part of a ''var_dump()'', ''(array)'' cast, or similar.+''var_dump()'', ''(array)'' casts, foreach and other functions/features inspecting object properties will only return properties that have a backing property, i.e. those using implicit accessors. Explicit accessor properties are omitted, and accessors will never be evaluated as part of a ''var_dump()'', ''(array)'' cast, foreach, or similar.
  
 <PHP> <PHP>
Line 902: Line 1000:
 //   int(42) //   int(42)
 // } // }
 +
 +var_dump((array) $test);
 +// array(1) {
 +//   ["prop1"]=>
 +//   int(42)
 +// }
 +
 +foreach ($test as $name => $value) {
 +    echo "$name: $value\n";
 +}
 +// prop1: 42
 </PHP> </PHP>
  
Line 921: Line 1030:
 </PHP> </PHP>
  
-This differs from the behavior of ''%%__get()%%'' and ''%%__set()%%'' on recursion, where we would instead fall back to behaving as is the ''%%__get()%%''/''%%__set()%%'' accessor were not present.+This differs from the behavior of ''%%__get()%%'' and ''%%__set()%%'' on recursion, where we would instead fall back to behaving as if the ''%%__get()%%''/''%%__set()%%'' accessor were not present.
  
 As we can only enter this kind of recursion if no backing property is present, using the same behavior as magic get/set would mean that a dynamic property with the same name of the accessor should be created. This is very inefficient, would make for confusing debug output, and is unlikely to be what the programmer intended. For that reason, we prohibit this kind of recursion instead. As we can only enter this kind of recursion if no backing property is present, using the same behavior as magic get/set would mean that a dynamic property with the same name of the accessor should be created. This is very inefficient, would make for confusing debug output, and is unlikely to be what the programmer intended. For that reason, we prohibit this kind of recursion instead.
Line 976: Line 1085:
 We can see that using a property with automatically generated accessors is slightly slower than one without accessors (though the impact might be higher on compound operations). Explicit accessors are much more expensive, but cheaper than magic methods. Getter/setter methods are significantly more expensive than implicit accessors, but also significantly cheaper than explicit accessors. We can see that using a property with automatically generated accessors is slightly slower than one without accessors (though the impact might be higher on compound operations). Explicit accessors are much more expensive, but cheaper than magic methods. Getter/setter methods are significantly more expensive than implicit accessors, but also significantly cheaper than explicit accessors.
  
-Explicit accessors are more expensive than methods, because they are executed through VM reentry, because the need to manage recursion guards, and because they go through the slow-path of property access.+Explicit accessors are more expensive than methods, because they are executed through VM reentry, need to manage recursion guards, and go through the slow-path of property access.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
Line 982: Line 1091:
 No backwards-incompatible changes are known. Due to the existence of magic get/set the introduction of accessors also shouldn't break existing assumptions much. One potential assumption break is that accessor properties cannot be unset. No backwards-incompatible changes are known. Due to the existence of magic get/set the introduction of accessors also shouldn't break existing assumptions much. One potential assumption break is that accessor properties cannot be unset.
  
-==== Reserved Keywords ====+==== Reserved keywords ====
  
 The accessor names ''get'' and ''set'' are **not** added as reserved keywords, and are contextually disambiguated instead. The accessor names ''get'' and ''set'' are **not** added as reserved keywords, and are contextually disambiguated instead.
Line 998: Line 1107:
  
 This is not a backwards-compatibility break, but mentioned for completeness. This is not a backwards-compatibility break, but mentioned for completeness.
 +
 +===== Discussion =====
 +
 +This is a fairly complex proposal, and is likely to grow more complex as remaining details are ironed out. I have some doubts that the complexity is truly justified.
 +
 +I think there are really two parts to this proposal, even if they are tightly related in the form presented here:
 +
 +  - Implicit accessors, which allow finely controlling property access. They support both read-only and private-write properties.
 +  - Explicit accessors, which allow implementing arbitrary behavior.
 +
 +My expectation is that implicit accessors will see heavy use, as read-only properties are a very common requirement. Explicit accessors on the other hand should be used rarely, and primarily as a means to maintain backwards-compatibility with an existing API. If it is known in advance that a property needs to be associated with non-trivial behavior, then it is preferable to implement it using methods in the first place.
 +
 +We could likely get 80% of the value of accessors by supporting read-only properties and 90% by also supporting private-write properties. A previous proposal for read-only properties was the [[rfc:write_once_properties|write-once properties RFC]]. While this particular proposal has been declined, I do think that the general approach was sound, and could be accepted.
  
 ===== Vote ===== ===== Vote =====
  
 +TBD.
rfc/property_accessors.1620061735.txt.gz · Last modified: 2021/05/03 17:08 by nikic