PHP RFC: Property Accessors
- Date: 2021-01-27
- Author: Nikita Popov nikic@php.net
- Proposed Version: PHP 8.1
- Implementation: https://github.com/php/php-src/pull/6873
- Status: Under Discussion
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 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:
class User { private $name; public function __construct(string $name) { $this->name = $name; } public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } }
With the introduction of typed properties in PHP 7.4, the use of getters and setters in this example no longer serves a useful purpose, and only increases the necessary boilerplate both in the class declaration, and for any consumers of the class. We could instead use a typed public property:
class User { public string $name; public function __construct(string $name) { $this->name = $name; } }
Or even make use of constructor property promotion:
class User { public function __construct(public string $name) {} }
This has one significant disadvantage: What happens if we later, for whatever reason, do want to introduce additional behavior for this property, for example by validating that it is non-empty? We could restore the original getters and setters, but that would constitute an API break. Or we could add the additional behavior through __get()
and __set()
:
class User { private string $_name; public function __construct(string $name) { $this->name = $name; } public function __get(string $propName): mixed { return match ($propName) { 'name' => $this->_name, default => throw new Error("Attempt to read undefined property $propName"), }; } public function __set(string $propName, $value): void { switch ($propName) { case 'name': if (!is_string($value)) { throw new TypeError("Name must be a string"); } if (strlen($value) === 0) { throw new ValueError("Name must be non-empty"); } $this->_name = $value; break; default: throw new Error("Attempt to write undefined property $propName"); } } public function __isset(string $propName): bool { return $propName === 'name'; } }
While doing this is possible, it has many disadvantages. Apart from requiring a lot of additional code, it also breaks reflection and static analysis, as the name
property does not really exist anymore. It is also necessary to reimplement your own type checks, which will not match PHP's behavior (with regard to coercive typing mode).
Property accessors allow you to introduce the additional behavior in a way that is specific to a single property, is reflectible and analyzable, and generally integrates well with the rest of PHP:
class User { private string $_name; public string $name { get { return $this->_name; } set { if (strlen($value) === 0) { throw new ValueError("Name must be non-empty"); } $this->_name = $value; } } public function __construct(string $name) { $this->name = $name; } }
Usage patterns
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 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, without a set
accessor:
class User { public string $name { get; } public function __construct(string $name) { $this->name = $name; } }
In this case, only one initial write is allowed to initialize the property. Afterwards, the property can only be read.
Asymmetric visibility
Proper read-only properties cannot be used in some cases, for example if the implementation uses “wither” methods using a clone and set implementation. In this case, it is useful to asymmetrically restrict accessor visibility instead:
class User { public string $name { get; private set; } public function __construct(string $name) { $this->name = $name; } public function withName(string $newName): static { $clone = clone $this; $clone->name = $newName; return $clone; } }
The assignment in withName()
works because a private set
accessor is available. The assignment would be forbidden outside the User
class.
Setter guard
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.
class User { public string $name { guard { if (strlen($value) === 0) { throw new ValueError("Name must be non-empty"); } } } }
Lazy initialization
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.
class Test { public string $somethingExpensive { lazy { return computeSomethingExpensive(); } set; } }
Synthesized properties
Finally, there are “real” accessor properties, which access a synthesized value. This could be in a read-only fashion, or a read-write fashion:
class Test { public int $value; public int $plusOne { get { return $this->value + 1; } } } class Test { public int $value; public int $plusOne { get { return $this->value + 1; } set { $this->value = $value - 1; } } }
Properties in interfaces
As accessors make properties a first-class citizen in class APIs, it also needs to be possible to declare properties in interfaces (or as abstract):
interface UserInterface { // Interface requires that property is public readable. public string $name { get; } } class User implements UserInterface { // Implemented without accessors, but (more than) satisfies the interface. public string $name; }
The interface declares a property that has to be at least readable, while the implementing class implements it in a way that is also writable, by using an ordinary property without accessors.
Proposal
Basics
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:
class Test { // Illegal, must define at least one accessor. public $prop { } // Read-only property. public $prop { get { /* ... */ } } // Write-only property. (Of dubious usefulness.) public $prop { set { /* ... */ } } // Read-write property. public $prop { get { /* ... */ } set { /* ... */ } } }
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.
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:
class Test { public int $prop { get { return 42; } // Effectively: public function get(): int { return 42; } } } $test = new Test; var_dump($test->prop); // int(42) // Modification of read-only property throws: $test->prop = 1; // Error: Property Test::$prop is read-only $test->prop += 1; // Error: Property Test::$prop is read-only
The set
accessor receives the new value of the property as the $value
variable, which is compatible with the property type:
class Test { public string $prop { set { echo "Set to $value\n"; } // Effectively: public function set(string $value): void { echo "Set to $value\n"; } } } $test = new Test; $test->prop = "foobar"; // "Set to foobar\n" // Reading of write-only property throws: var_dump($test->prop); // Error: Property Test::$prop is write-only var_dump(isset($test->prop)); // Error: Property Test::$prop is write-only
The default name of $value
can be changed by explicitly specifying the parameter name:
class Test { public string $prop { set($someOtherName) { echo "Set to $someOtherName\n"; } } }
Write-only properties like the above example have dubious usefulness. The more typical case is where a property defines both get
and set
:
class Test { public int $value = 0; public int $valuePlusOne { get { return $value + 1; } set { $this->value = $value - 1; } } } $test = new Test; $this->value = 9; var_dump($this->valuePlusOne); // int(10) $this->valuePlusOne = 42; var_dump($this->value); // int(41) $this->valuePlusOne += 5; // Behaves like: $this->valuePlusOne = $this->valuePlusOne + 5;
It is not permitted to specify any explicit types on accessor methods, as they are inferred from the type of the property. Code like the following is illegal:
class Test { public string $prop { get(): string { return ""; } // Can't have return type (it's implicit) set(string $value): void { } // Can't have argument or return type (it's implicit) } }
The following signatures are also illegal:
get() {} // Must not have parameter list set() {} // Must have exactly one parameter set($a, $b) {} // ... if a parameter list is specified set(...$a) {} // Cannot be variadic set(&$a) {} // Cannot be by-reference set($a = 1) {} // Cannot have default
Specifying the same accessor multiple times is also illegal.
By-reference getter
Similar to __get()
, the get
accessor can either return by value or by reference:
class Test { public $byVal { get; set; } public $byRef { &get; set; } }
By-value get and set supports increment/decrement and compound assignment operators:
$test = new Test; // All of these work: $test->byVal = 0; $test->byVal++; --$test->byVal; $test->byVal += 2;
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:
$test = new Test; $test->byVal = []; $test->byVal[] = 1; // Warning: Indirect modification has no effect $ref =& $test->byVal; // Warning: Indirect modification has no effect
Assigning a reference to an accessor property will always fail:
$test->byRef =& $ref; // Error: Cannot assign by reference to overloaded object
This operations is not supported, and cannot be supported without introduction of an additional accessor type for by-reference assignments.
It it not permitted to specify only a by-reference &get
accessor.
class Test { // Illegal: Only by-ref get public array $prop { &get; } // Should be either public array $prop { get; } // Or public array $prop { &get; set; } }
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:
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
It is not possible to define isset
or unset
accessors. isset($obj->accessorProp)
is equivalent to $obj->accessorProp !== null
and unset($obj->accessorProp)
always throws an Error
exception.
Static properties
Accessors for static properties are not supported at this time.
class Test { // Illegal: Accessors on static property. public static $prop { get; set; } }
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
The visibility of the accessors defaults to the visibility of the property, but may be explicitly overridden for individual accessors, resulting in asymmetric visibility:
class Test { public string $prop { get; private set; } public function __construct(string $prop) { $this->prop = $prop; } } $test = new Test("foo"); var_dump($test->prop); // Works. $test->prop = "bar"; // Error: Call to private accessor Test::$prop::set() from global scope
Visibility on individual accessors must either be omitted, or strictly smaller than the property visibility:
class Test { // Illegal: "public get" has higher visibility than "private $prop". private string $prop { public get; set; } // Illegal: "public get" has same visibility as "public $prop". // This visibility modifier is redundant and must be omitted. public string $prop { public get; private set; } }
Interaction with magic methods
If a property is not visible, then interactions with it will fall back to calling __get()
and __set()
magic methods. This also holds true if the property as a whole is visible, but an individual accessor is not:
class Test { public $prop { get; private set; } public function __construct($prop) { $this->prop = $prop; } public function __get($name) { echo "__get($name)\n"; return $this->$name; } public function __set($name, $value) { echo "__set($name, $value)\n"; $this->$name = $value; } } $test = new Test(1); $test->prop = 2; // Calls Test::__set("prop", 2) and then Test::$prop::set(2) var_dump($test->prop); // Calls Test::$prop::get()
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:
class Test { public array $prop = [] { &get; private set; } } $test = new Test; $test->prop[] = 42; // Allowed! $ref =& $test->prop; $ref = [1, 2, 3]; // Allowed!
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
Acessors can be inherited with similar semantics to normal methods, and support abstract
and final
modifiers.
Simple inheritance
Accessors from the child class take precedence, but accessors that have not been explicitly overridden will be taken from the parent class:
class A { public $prop { get { echo __METHOD__, "\n"; } set { echo __METHOD__, "\n"; } } } class B extends A { public $prop { set { echo __METHOD__, "\n"; } } } $b = new B; $b->prop; // A::$prop::get (inherited) $b->prop = 1; // B::$prop::set (overridden)
Property compatibility
For the most part, accessor properties follow the same compatibility rules as normal properties (e.g. visibility may not be reduced). However, there are some additional considerations.
Normal properties are invariant in the type system. Read-only accessor properties are covariant, while write-only accessor properties are contravariant.
class A { public int|string $invariant { get; set; } public int|string $covariant { get; } // This property is useless, but will serve for the sake of illustration. public int|string $contravariant { set { /* ... */ } } } class B extends A { // Illegal: int is not subtype of int|string. public int $invariant; // Illegal: int|float|string is not subtype of int|string. public int|float|string $invariant; // Legal: int is subtype of int|string. public int $covariant; // Illegal: int|float|string is not subtype of int|string. public int|float|string $covariant; // Illegal: int|string is not a subtype of int. public int $contravariant; // Legal: int|string is a subtype of int|float|string. public int|float|string $contravariant; }
In practical terms, this means that the type of a read-only property may be narrowed. Widening the type of a write-only property is a largely theoretical property.
Similarly to normal methods, if the parent get
returns by reference, then the child get
is also required to return by reference:
class A { public $prop { &get; set; } } class B extends A { // Illegal: get must return by ref. public $prop { get; set; } // Legal public $prop { &get { /* My new get */ } } }
It is possible to override an accessor property with a normal property:
class A { public $prop { get { echo __METHOD__, "\n"; } set { echo __METHOD__, "\n"; } } } class B extends A { // Legal. public $prop; }
This is allowed, as a normal property can be used anywhere an accessor property can be used. However, the converse does not hold, and the following is illegal:
class A { public $prop; } class B extends A { public $prop { &get; set; } }
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:
$b = new B; $b->prop =& $prop; // Error: Cannot assign by reference to overloaded object
Final properties and accessors
Accessors can be marked as final, in which case they cannot be overridden in child classes:
class A { public $prop { get { echo __METHOD, "\n"; } final set { echo __METHOD, "\n"; } } } // Legal: A::$prop::get() can be overridden. class B extends A { public $prop { get { echo __METHOD, "\n"; } } } // Illegal: A::$prop::set() is final. class C extends A { public $prop { set { echo __METHOD, "\n"; } } }
A whole property can also be marked final. This will prohibit any redeclaration in child classes, even if accessors are only added:
class A { final public $prop1; final public $prop2 { get; } } class B extends A { // Illegal, properties are final. public $prop1 { get; set; } public $prop2 { set; } }
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.
class Test { // Illegal, private and final property. final private $prop; // Illegal, private and final accessor. 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; } }
Redundant final modifiers (on both the property and an accessor) are illegal.
Abstract properties and accessors
Accessors can be marked abstract, in which case the class must also be abstract, and the accessor needs to be implemented by any (non-abstract) child class.
abstract class A { public $prop { get { echo __METHOD__, "\n"; } abstract set; } } // Illegal, missing implementation for A::$prop::set(). class B extends A { } // Legal, all abstract accessors implemented. class C extends A { public $prop { set { echo __METHOD__, "\n"; } } }
A whole property can be marked abstract, which is the same as marking all accessors abstract:
abstract class A { abstract public $prop { get; set; } } // Legal implementation. class B extends A { public $prop; }
It is not legal to mark a non-accessor property as abstract. It is always required to specify which accessors an abstract property needs to satisfy.
abstract class A { // Illegal, only accessor properties can be abstract. abstract public $prop; }
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:
interface I { public $readonly { get; } public $readwrite { get; set; } } class C implements I { public $readonly { get { return "Foo"; } } public $readwrite; }
Accessor properties in interfaces can only be public, and cannot be explicitly abstract (they are implicitly abstract).
Traits
Accessor properties may be included in traits. However, any form of conflict resolution between accessors from different traits is not supported and will result in a fatal error:
trait T1 { public $prop { get; set; } } trait T2 { public $prop { get; set; } } class C { // Fatal error: T1 and T2 define the same accessor property ($prop) in the composition of C. // Conflict resolution between accessor properties is currently not supported. use T1, T2; }
While we could implicitly resolve conflicts between implicit accessors, resolving them for explicit accessors would require syntactic support similar to that of methods. As the required complexity is likely not commensurate with the frequency of usage, this has been omitted for the time being.
It is worth noting that this applies even if a property from a trait is used and overridden in the same class:
trait T { public $prop { get; set; } } class C { use T; public $prop { get; set; } }
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:
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
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:
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; }
TODO: The current implementation does not handle this case correctly.
TODO: Parent accessors
An open question is how parent accessors may be invoked. A possible syntax is to use parent::$prop
for the parent getter and parent::$prop = $value
for the parent setter. As the “static-ness” of properties cannot change, this reference should be unambiguous.
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 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.
Implicit implementation
If get
and set
accessors are specified without an implementation, then an implementation is generated automatically. The automatic implementation will forward to an implicit backing property:
class Test { public string $prop { get; set; } } // Is conceptually similar to: class Test { private string $_prop; public string $prop { get { return $this->_prop; } set { $this->_prop = $value; } } }
The actual backing property however has the same name as the accessor property, and will appear as such in var_dump()
output and similar.
While the implementation will handle such accessors much more efficiently (and will not be performing any actual method calls), they are still subject to normal accessor restrictions. For example, get
and &get
will have distinct behavior (the latter will allow indirect modification, and the former won't).
A main purpose of auto-implemented accessors is to specify an asymmetric visibility, without changing any behavior:
class Test { public string $prop { get; private set; } }
It is not possible to only specify an auto-implemented set
accessor:
class Test { // Illegal: Cannot have only implicit set. public string $prop { set; } }
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:
class Test { public string $prop { get; } public function __construct(string $prop) { $this->prop = $prop; // Works (initializing assignment) } } $test = new Test("init"); var_dump($test->prop); // Works (read) $test->prop = "foo"; // Error: Property Test::$prop is read-only
It is not possible to mix an implicit get with an explicit set, or vice versa:
class Test { // Illegal: Implicit get, explicit set. public string $prop { get; set { /* ... */ } } // Illegal: Implicit set, explicit get. public string $prop { get { /* ... */ } set; } }
Accessor properties that do not use implicit implementation will also not have an implicit backing property. This means that such properties take up no space in the object (by themselves). The programmer is required to manage any necessary backing storage themselves.
Unless otherwise specified, the default value for accessor properties is always uninitialized, and an Error
will be thrown if the property is read prior to initialization. This holds even if the accessor property has no explicit type. However, it is possible to specify a default value explicitly:
class Test { public string $prop = "" { get; set; } public $prop2 = null { get; set; } }
It should be noted that if a default value is specified for a read-only accessor property, then that default value is considered an initialization, and cannot be changed.
A default value cannot be specified for properties with explicit accessors, as PHP does not manage storage for such properties:
class Test { // Illegal: Default value on property with explicit accessors. public $prop = "" { get { /* ... */ } } }
Constructor promotion
If only implicitly implemented accessors are used, then accessor properties can be used in conjunction with constructor promotion:
class Test { public function __construct( public string $prop { get; private set; }, public int $prop2 = 0 { get; }, ) {} }
Constructor promotion cannot be used with explicitly implemented accessors. The following code is illegal:
class Test { public function __construct( public string $prop { get { return ""; } set { echo "Set!\n"; } } ) {} }
This limitation exists to prevent embedding of very large property declarations in the constructor signature.
var_dump(), array cast, foreach etc
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.
class Test { public $prop1 = 42 { get; set; } public $prop2 { get { return 123; } } } $test = new Test; var_dump($test); // object(Test)#1 (1) { // ["prop1"]=> // int(42) // } var_dump((array) $test); // array(1) { // ["prop1"]=> // int(42) // } foreach ($test as $name => $value) { echo "$name: $value\n"; } // prop1: 42
Recursion
If an accessor ends up accessing the property recursively, an Error
is thrown:
class Test { public string $prop { get { return $this->prop; } set { $this->prop = $value; } } } $test = new Test; $test->prop = 1; // Error: Cannot recursively write Test::$prop in accessor $test->prop; // Error: Cannot recursively read Test::$prop in accessor
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.
Reflection
The following members are added to ReflectionProperty
:
class ReflectionProperty { const IS_FINAL; const IS_ABSTRACT; public function isFinal(): bool {} public function isAbstract(): bool {} public function getGet(): ?ReflectionMethod {} public function getSet(): ?ReflectionMethod {} }
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
This script provides a basic benchmark for the different property kinds. It produces the following rough results:
# public $prop1 Normal property read: 0.047155s Normal property write: 0.054694s # public $prop2 { get; set; } Auto accessor property read: 0.053085s Auto accessor property write: 0.054916s # public $prop3 { get { ... } set { ... } } Accessor property read: 0.361496s Accessor property write: 0.403377s # __get() and __set() Magic property read: 0.448467s Magic property write: 0.504917s # getProp() and setProp() Method property read: 0.122556s Method property write: 0.154577s
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, need to manage recursion guards, and go through the slow-path of property access.
Backward Incompatible Changes
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.
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.
// 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 "{"
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 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
TBD.