rfc:property_accessors
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:property_accessors [2021/05/03 13:19] – nikic | rfc:property_accessors [2022/04/28 09:30] (current) – Fix typos ilutov | ||
---|---|---|---|
Line 3: | Line 3: | ||
* Author: Nikita Popov < | * Author: Nikita Popov < | ||
* Proposed Version: PHP 8.1 | * Proposed Version: PHP 8.1 | ||
- | * Implementation: | + | * Implementation: |
- | * Status: | + | * Status: |
===== Introduction ===== | ===== Introduction ===== | ||
- | Property accessors allow implementing custom behavior for reading or writing a property. PHP already provides this general functionality through '' | + | Property accessors allow implementing custom behavior for reading or writing a property. PHP already provides this general functionality through '' |
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, | 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, | ||
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 | + | === Read-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 '' | + | 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 '' |
<PHP> | <PHP> | ||
Line 155: | Line 155: | ||
</ | </ | ||
- | In this case, the assignment in '' | + | The assignment in '' |
=== Setter guard === | === Setter guard === | ||
- | This is the use-case mentioned in the introduction: | + | //This is not part of the current proposal.// |
+ | |||
+ | This is the use-case mentioned in the introduction: | ||
<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 '' | + | //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 '' | ||
<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 { |
+ | | ||
+ | | ||
// Write-only property. (Of dubious usefulness.) | // Write-only property. (Of dubious usefulness.) | ||
- | public $prop { set; } | + | public $prop { |
+ | | ||
+ | | ||
// Read-write property. | // Read-write property. | ||
- | public $prop { get; set; } | + | public $prop { |
+ | | ||
+ | | ||
+ | | ||
} | } | ||
</ | </ | ||
Line 255: | Line 266: | ||
The basic accessors are '' | The basic accessors are '' | ||
- | The above example | + | Accessors can use an implicit or an explicit implementation. Implicit implementation |
+ | |||
+ | If an explicit implementation is provided, '' | ||
<PHP> | <PHP> | ||
Line 275: | Line 288: | ||
</ | </ | ||
- | The '' | + | The '' |
<PHP> | <PHP> | ||
Line 350: | Line 363: | ||
</ | </ | ||
- | Specifying the same accessor multiple | + | Specifying the same accessor multiple |
=== By-reference getter === | === By-reference getter === | ||
Line 374: | Line 387: | ||
</ | </ | ||
- | These indirect | + | These indirect |
However, indirect array modification, | However, indirect array modification, | ||
Line 407: | Line 420: | ||
While nominally well-defined, | While nominally well-defined, | ||
+ | |||
+ | TODO: An open problem is how implicit by-value '' | ||
+ | |||
+ | <PHP> | ||
+ | class Test { | ||
+ | public $prop = null { get; private set; } | ||
+ | } | ||
+ | |||
+ | $test = new Test; | ||
+ | foreach ($test as &$prop) { | ||
+ | $prop = 1; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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: | ||
</ | </ | ||
- | This is in line with current magic method, where '' | + | This is in line with current magic methods, where '' |
==== Visibility ==== | ==== Visibility ==== | ||
Line 483: | Line 511: | ||
var_dump($test-> | var_dump($test-> | ||
</ | </ | ||
+ | |||
+ | === Interaction with by-reference getter === | ||
+ | |||
+ | It is worth pointing out that the combination of ''& | ||
+ | |||
+ | <PHP> | ||
+ | class Test { | ||
+ | public array $prop = [] { &get; private set; } | ||
+ | } | ||
+ | $test = new Test; | ||
+ | $test-> | ||
+ | $ref =& $test-> | ||
+ | $ref = [1, 2, 3]; // Allowed! | ||
+ | </ | ||
+ | |||
+ | It would be possible to make this do something more meaningful by making the " | ||
==== Inheritance ==== | ==== Inheritance ==== | ||
Line 495: | Line 539: | ||
class A { | class A { | ||
public $prop { | public $prop { | ||
- | get { echo __METHOD, " | + | get { echo __METHOD__, " |
- | set { echo __METHOD, " | + | set { echo __METHOD__, " |
} | } | ||
} | } | ||
class B extends A { | class B extends A { | ||
public $prop { | public $prop { | ||
- | set { echo __METHOD, " | + | set { echo __METHOD__, " |
} | } | ||
} | } | ||
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: | ||
</ | </ | ||
- | 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 ''& | + | 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 ''& |
<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 | ||
</ | </ | ||
- | |||
- | As proposed, there are also other limitations, | ||
=== Final properties and accessors === | === Final properties and accessors === | ||
Line 623: | Line 665: | ||
<PHP> | <PHP> | ||
class A { | class A { | ||
- | | + | |
- | final public $prop { get; } | + | final public $prop2 { get; } |
} | } | ||
class B extends A { | class B extends A { | ||
- | // Illegal, | + | // Illegal, |
- | public $prop { set; } | + | public $prop1 { get; set; } |
+ | public $prop2 | ||
} | } | ||
</ | </ | ||
- | Marking a property/accessor both private and final is illegal. | + | Marking a property |
<PHP> | <PHP> | ||
class Test { | class Test { | ||
- | // Illegal, private and final. | + | // Illegal, private and final property. |
- | | + | final private |
| | ||
- | // Illegal, | + | // Illegal, |
- | | + | public $prop { final private |
+ | |||
+ | // Illegal, private and final accessor. | ||
+ | private $prop { final get; } | ||
+ | |||
+ | // Legal, final property with private accessor. | ||
+ | final public $prop { get; private | ||
} | } | ||
</ | </ | ||
+ | |||
+ | Redundant final modifiers (on both the property and an accessor) are illegal. | ||
=== Abstract properties and accessors === | === Abstract properties and 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 " | 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 " | ||
+ | |||
+ | === 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__, " | ||
+ | private set { echo __METHOD__, " | ||
+ | } | ||
+ | |||
+ | public function test() { | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class B extends A { | ||
+ | public $prop { | ||
+ | get { echo __METHOD__, " | ||
+ | set { echo __METHOD__, " | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $a = new A; | ||
+ | $a-> | ||
+ | $b = new B; | ||
+ | $b-> | ||
+ | |||
+ | // Prints: | ||
+ | // A:: | ||
+ | // A:: | ||
+ | // B:: | ||
+ | // A:: | ||
+ | </ | ||
+ | |||
+ | When accessed on an instance of '' | ||
+ | |||
+ | 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__, " | ||
+ | private set { echo __METHOD__, " | ||
+ | } | ||
+ | |||
+ | public function test() { | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class B extends A { | ||
+ | public $prop; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 '' | 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 '' | ||
- | Other syntax choices like '' | + | Other syntax choices like '' |
In any case, adding support for parent accessors will be technically non-trivial, | In any case, adding support for parent accessors will be technically non-trivial, | ||
Line 788: | Line 899: | ||
</ | </ | ||
- | However, it is possible to specify only a '' | + | However, it is possible to specify only a '' |
<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 { |
+ | | ||
+ | | ||
+ | | ||
// Illegal: Implicit set, explicit get. | // Illegal: Implicit set, explicit get. | ||
- | public string $prop { get {} set; } | + | public string $prop { |
+ | | ||
+ | | ||
+ | | ||
} | } | ||
</ | </ | ||
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 |
- | '' | + | '' |
<PHP> | <PHP> | ||
Line 883: | Line 1000: | ||
// | // | ||
// } | // } | ||
+ | |||
+ | var_dump((array) $test); | ||
+ | // array(1) { | ||
+ | // | ||
+ | // | ||
+ | // } | ||
+ | |||
+ | foreach ($test as $name => $value) { | ||
+ | echo " | ||
+ | } | ||
+ | // prop1: 42 | ||
</ | </ | ||
Line 902: | Line 1030: | ||
</ | </ | ||
- | This differs from the behavior of '' | + | This differs from the behavior of '' |
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, | 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, | ||
+ | |||
+ | ==== Reflection ==== | ||
+ | |||
+ | The following members are added to '' | ||
+ | |||
+ | <PHP> | ||
+ | class ReflectionProperty { | ||
+ | const IS_FINAL; | ||
+ | const IS_ABSTRACT; | ||
+ | | ||
+ | public function isFinal(): bool {} | ||
+ | public function isAbstract(): | ||
+ | |||
+ | public function getGet(): ? | ||
+ | public function getSet(): ? | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The '' | ||
+ | |||
+ | The '' | ||
+ | |||
+ | TODO: Should '' | ||
===== 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/ | 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/ | ||
- | Explicit accessors are more expensive than methods, because they are executed through VM reentry, | + | 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 940: | Line 1091: | ||
No backwards-incompatible changes are known. Due to the existence of magic get/set the introduction of accessors also shouldn' | No backwards-incompatible changes are known. Due to the existence of magic get/set the introduction of accessors also shouldn' | ||
- | ==== Reserved | + | ==== Reserved |
The accessor names '' | The accessor names '' | ||
Line 956: | 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: | ||
===== Vote ===== | ===== Vote ===== | ||
+ | TBD. |
rfc/property_accessors.1620047990.txt.gz · Last modified: 2021/05/03 13:19 by nikic