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 14:56]
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 266: Line 266:
 The basic accessors are ''get'' and ''set'', which are invoked when the property is read or written respectively. If an accessor is omitted, then performing the corresponding operation on the property will result in an ''Error'' exception. The basic accessors are ''get'' and ''set'', which are invoked when the property is read or written respectively. If an accessor is omitted, then performing the corresponding operation on the property will result in an ''Error'' exception.
  
-The above example uses automatically generated accessors, which are discussed in the "Implicit implementation" section. If an explicit implementation is provided, ''get'' should return the value of the property, which must satisfy the type of the property under the usual rules:+Accessors can use an implicit or an explicit implementation. Implicit implementation uses ''get;'' and ''set;'' with an auto-generated accessor implementation. This is further discussed in the "Implicit implementation" section. 
 + 
 +If an explicit implementation is provided, ''get'' should return the value of the property, which must satisfy the type of the property under the usual rules:
  
 <PHP> <PHP>
Line 286: Line 288:
 </PHP> </PHP>
  
-The ''set'' handle receives the new value of the property as the ''$value'' variable, which is compatible with the property type:+The ''set'' accessor receives the new value of the property as the ''$value'' variable, which is compatible with the property type:
  
 <PHP> <PHP>
Line 361: 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 385: 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 418: 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 434: Line 451:
 </PHP> </PHP>
  
-This is in line with current magic method, where ''%%__get()%%'' and ''%%__set()%%'' only support instance properties, but not static properties. Adding support for static property accessors is possible, at the expense of additional implementation effort.+This is in line with current magic methods, where ''%%__get()%%'' and ''%%__set()%%'' only support instance properties, but not static properties. Adding support for static property accessors is possible, at the expense of additional implementation effort.
  
 ==== Visibility ==== ==== Visibility ====
Line 494: 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 506: 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 530: Line 563:
 class A { class A {
     public int|string $invariant { get; set; }     public int|string $invariant { get; set; }
-    public int|string $covariant { set; }+    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 596: Line 629:
 </PHP> </PHP>
  
-This restriction exists, because accessors, even in their most general form, do not and can not support certain behavior. In particular, while it is possible to take a reference to an accessor property (as long as it uses ''&get''), it's not possible to assign to such property by reference:+This restriction exists, because accessors, even in their most general form, do not support certain behavior. In particular, while it is possible to take a reference to an accessor property (as long as it uses ''&get''), it's not possible to assign a reference //to// an accessor property:
  
 <PHP> <PHP>
Line 602: Line 635:
 $b->prop =& $prop; // Error: Cannot assign by reference to overloaded object $b->prop =& $prop; // Error: Cannot assign by reference to overloaded object
 </PHP> </PHP>
- 
-As proposed, there are also other limitations, e.g. the inability to ''unset()'' accessor properties (this is a design decision though, and not fundamental). 
  
 === Final properties and accessors === === Final properties and accessors ===
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>
  
-Marking a property/accessor both private and final is illegal. Redundant final modifiers are illegal.+Marking a property or accessor both private and final is illegal. However, it is explicitly allowed to have a final property with a private accessor.
  
 <PHP> <PHP>
 class Test { class Test {
-    // Illegal, private and final. +    // Illegal, private and final property
-    private final $prop;+    final private $prop;
          
-    // Illegal, redundant final. +    // Illegal, private and final accessor
-    final public $prop { final get; final set; }+    public $prop { final private get; 
 +     
 +    // Illegal, private and final accessor. 
 +    private $prop { final get; } 
 +     
 +    // Legal, final property with private accessor. 
 +    final public $prop { get; private set; }
 } }
 </PHP> </PHP>
 +
 +Redundant final modifiers (on both the property and an accessor) are illegal.
  
 === Abstract properties and accessors === === Abstract properties and accessors ===
Line 749: 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 756: 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 799: 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 820: 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 845: 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 878: 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 894: 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 913: 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 919: Line 1036:
 ==== Reflection ==== ==== Reflection ====
  
-Two methods are added to ''ReflectionProperty'', which can be used to retrieve the accessor methods:+The following members are added to ''ReflectionProperty'':
  
 <PHP> <PHP>
 class ReflectionProperty { class ReflectionProperty {
 +    const IS_FINAL;
 +    const IS_ABSTRACT;
 +    
 +    public function isFinal(): bool {}
 +    public function isAbstract(): bool {}
 +
     public function getGet(): ?ReflectionMethod {}     public function getGet(): ?ReflectionMethod {}
     public function getSet(): ?ReflectionMethod {}     public function getSet(): ?ReflectionMethod {}
Line 928: Line 1051:
 </PHP> </PHP>
  
-The methods return ''null'' if property does not use accessors, or does not specify this particular accessor. For non-abstract accessors, it is possible to invoke the method, even if it is implicitly implemented.+The ''isFinal()'' and ''isAbstract()'' methods determine whether the property as a whole (rather than an individual accessor) is marked as final or abstract respectively. ''getModifiers()'' will now also return these modifiers, and ''ReflectionProperty::IS_FINAL'' and ''ReflectionProperty::IS_ABSTRACT'' are added as the corresponding bit flags. 
 + 
 +The ''getGet()'' and ''getSet()'' methods return the corresponding accessor if it exists. For non-abstract accessors, it is possible to invoke the method, even if it is implicitly implemented
 + 
 +TODO: Should ''getGet()'' and ''getSet()'' return an inherited private accessor? The reflection API is currently inconsistent when it comes to this.
  
 ===== Performance ===== ===== Performance =====
Line 958: 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 964: 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 980: 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.1620053792.txt.gz · Last modified: 2021/05/03 14:56 by nikic