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 13:12]
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 =====
  
-Property accessors allow implementing custom behavior for reading or writing a property. PHP already provides this general functionality through ''%%__get()%%'' and ''%%__set()%%''. However, these methods are non-specific and may be used to intercept all property accesses. This RFC proposes to add per-property accessors.+Property accessors allow implementing custom behavior for reading or writing a property. PHP already provides this general functionality through ''%%__get()%%'' and ''%%__set()%%''. However, these methods are non-specific and may only be used to intercept //all// undefined/inaccessible property accesses. This RFC proposes to add per-property accessors.
  
 The primary use case for accessors is actually to **not** use them, but retain the ability to do so in the future, should it become necessary. Consider the following class declaration, which might have been considered idiomatic prior to PHP 7.4: The primary use case for accessors is actually to **not** use them, but retain the ability to do so in the future, should it become necessary. Consider the following class declaration, which might have been considered idiomatic prior to PHP 7.4:
Line 119: Line 119:
 The following section illustrates various usage patterns for accessors. This section is motivational and not normative: Not all examples are supported by the proposal in its current form, though it's possible to achieve them in other ways. The following section illustrates various usage patterns for accessors. This section is motivational and not normative: Not all examples are supported by the proposal in its current form, though it's possible to achieve them in other ways.
  
-=== Read-only property ===+=== Read-only properties ===
  
-One of the most important use-cases for accessors are read-only properties without additional behavior changes. These can be achieved by using an automatically implemented ''get'' accessor only:+One of the most important use-cases for accessors are read-only properties without additional behavior changes. These can be achieved by using an automatically implemented ''get'' accessor, without a ''set'' accessor:
  
 <PHP> <PHP>
Line 155: Line 155:
 </PHP> </PHP>
  
-In this case, the assignment in ''withName()'' works because a ''private set'' accessor is available. The assignment would be forbidden outside the ''User'' class.+The assignment in ''withName()'' works because a ''private set'' accessor is available. The assignment would be forbidden outside the ''User'' class.
  
 === Setter guard === === Setter guard ===
  
-This is the use-case mentioned in the introduction: Adding additional validation checks when setting a property. It would be possible to support this using a first-class ''guard'' accessor, which is invoked before the value is set. It would allow introducing an additional check, while retaining automatic management of the property storage. This is not part of the current proposal.+//This is not part of the current proposal.// 
 + 
 +This is the use-case mentioned in the introduction: Adding additional validation checks when setting a property. It would be possible to support this using a first-class ''guard'' accessor, which is invoked before the value is set. It would allow introducing an additional check, while retaining automatic management of the property storage.
  
 <PHP> <PHP>
Line 175: Line 177:
 === Lazy initialization === === Lazy initialization ===
  
-For values that are expensive to compute, it may be useful to lazily initialize a property the first time it is accessed. This could be handled through a first-class ''lazy'' accessor, that is invoked the first time a property is read. This is not part of the current proposal.+//This is not part of the current proposal.// 
 + 
 +For values that are expensive to compute, it may be useful to lazily initialize a property the first time it is accessed. This could be handled through a first-class ''lazy'' accessor, that is invoked the first time a property is read.
  
 <PHP> <PHP>
Line 235: Line 239:
 ==== Basics ==== ==== Basics ====
  
-To declare an accessor property, the trailing semicolon of a property declaration is replaced by accessor list, which must contain at least one accessor:+To declare an accessor property, the trailing semicolon of a property declaration is replaced by an accessor list, which must contain at least one accessor:
  
 <PHP> <PHP>
Line 243: Line 247:
  
     // Read-only property.     // Read-only property.
-    public $prop { get}+    public $prop { 
 +        get { /* ... */ } 
 +    }
  
     // Write-only property. (Of dubious usefulness.)     // Write-only property. (Of dubious usefulness.)
-    public $prop { set}+    public $prop { 
 +        set { /* ... */ } 
 +    }
  
     // Read-write property.     // Read-write property.
-    public $prop { getset}+    public $prop { 
 +        get { /* ... */ } 
 +        set { /* ... */ } 
 +    }
 } }
 </PHP> </PHP>
Line 255: 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 275: 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 350: 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 374: 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 407: 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 423: 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 483: 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 495: 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 519: 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 585: 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 591: 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 623: 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 688: Line 739:
 </PHP> </PHP>
  
-Properties / accessors cannot be both abstract and private. Redundant abstract modifiers (on both the property and the accessor) cannot be specified. Abstract accessors cannot have bodies.+Properties / accessors cannot be both abstract and private, or abstract and final. Redundant abstract modifiers (on both the property and the accessor) cannot be specified. Abstract accessors cannot have bodies.
  
 Accessor properties can also be part of interfaces, in which case they follow the rules of abstract accessors: Accessor properties can also be part of interfaces, in which case they follow the rules of abstract accessors:
Line 738: 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 745: 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 788: 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 809: 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 834: 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 867: 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 883: 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 902: 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.
 +
 +==== Reflection ====
 +
 +The following members are added to ''ReflectionProperty'':
 +
 +<PHP>
 +class ReflectionProperty {
 +    const IS_FINAL;
 +    const IS_ABSTRACT;
 +    
 +    public function isFinal(): bool {}
 +    public function isAbstract(): bool {}
 +
 +    public function getGet(): ?ReflectionMethod {}
 +    public function getSet(): ?ReflectionMethod {}
 +}
 +</PHP>
 +
 +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 934: 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 =====
  
-==== Reserved Keywords ====+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 ====
  
 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.
 +
 +==== Alternative array syntax ====
 +
 +The alternative array syntax ''$foo{$idx}'' has already been dropped in PHP 8.0. However, support for it was retained in the parser, in order to generate a nicer error message. Support for accessors requires this parser support to be dropped to avoid parser ambiguities when default values and accessors are used at the same time.
 +
 +<PHP>
 +// Before:
 +$foo{0}; // Fatal error: Array and string offset access syntax with curly braces is no longer supported
 +// After:
 +$foo{0}; // Parse error: syntax error, unexpected token "{"
 +</PHP>
 +
 +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.1620047525.txt.gz · Last modified: 2021/05/03 13:12 by nikic