diff --git a/language/constants.xml b/language/constants.xml
index 30a275db0b1f..40c9f0ff1f7b 100644
--- a/language/constants.xml
+++ b/language/constants.xml
@@ -321,6 +321,12 @@ echo ANIMALS[1]; // outputs "cat"
The class method name.
+
+ __PROPERTY__
+
+ Only valid inside a property hook. It is equal to the name of the property.
+
+ __NAMESPACE__
diff --git a/language/oop5.xml b/language/oop5.xml
index 7ddee2abc516..c690e49c4299 100644
--- a/language/oop5.xml
+++ b/language/oop5.xml
@@ -26,6 +26,7 @@
&language.oop5.basic;
&language.oop5.properties;
+ &language.oop5.property-hooks;
&language.oop5.constants;
&language.oop5.autoload;
&language.oop5.decon;
diff --git a/language/oop5/abstract.xml b/language/oop5/abstract.xml
index 440b875f0d06..d612d7a49a83 100644
--- a/language/oop5/abstract.xml
+++ b/language/oop5/abstract.xml
@@ -4,11 +4,13 @@
Class Abstraction
- PHP has abstract classes and methods.
+ PHP has abstract classes, methods, and properties.
Classes defined as abstract cannot be instantiated, and any class that
- contains at least one abstract method must also be abstract.
- Methods defined as abstract simply declare the method's signature;
- they cannot define the implementation.
+ contains at least one abstract method or property must also be abstract.
+ Methods defined as abstract simply declare the method's signature and whether it is public or protected;
+ they cannot define the implementation. Properties defined as abstract
+ may declare a requirement for get or set
+ behavior, and may provide an implementation for one, but not both, operations.
@@ -19,8 +21,18 @@
signature compatibility rules.
+
+ As of PHP 8.4, an abstract class may declare an abstract property, either public or protected.
+ A protected abstract property may be satisfied by a property that is readable/writeable from either
+ protected or public scope.
+
+
+ An abstract property may be satisfied either by a standard property or by a property
+ with defined hooks, corresponding to the required operation.
+
+
- Abstract class example
+ Abstract method example
- Abstract class example
+ Abstract method example
+
+ Abstract property example
+
+ $value;
+ }
+
+ // This expands the visibility from protected to public, which is fine.
+ public string $both;
+}
+?>
+]]>
+
+
+
+ An abstract property on an abstract class may provide implementations for any hook,
+ but must have either get or set declared but not defined (as in the example above).
+
+
+ Abstract property example
+
+foo = $value };
+ }
+}
+?>
+ ]]>
+
+
+
+ Property Hooks
+
+
+ Property hooks, also known as "property accessors" in some other languages,
+ are a way to intercept and override the read and write behavior of a property.
+ This functionality serves two purposes:
+
+
+
+
+ It allows for properties to be used directly, without get- and set- methods,
+ while leaving the option open to add additional behavior in the future.
+ That renders most boilerplate get/set methods unnecessary, even without
+ using hooks.
+
+
+
+
+ It allows for properties that describe an object without needing to store
+ a value directly.
+
+
+
+
+ There are two hooks available on all properties: get and set.
+ They allow overriding the read and write behavior of a property, respectively.
+
+
+ A property may be "backed" or "virtual". A backed property
+ is one that actually stores a value. Any property that has no hooks is backed.
+ A virtual property is one that has hooks and those hooks do not interact with
+ the property itself. In this case, the hooks are effectively the same as methods,
+ and the object does not use any space to store a value for that property.
+
+
+ Property hooks are incompatible with readonly properties.
+ If there is a need to restrict access to a get or set
+ operation in addition to altering its behavior, use
+ asymmetric property visibility.
+
+
+ Basic Hook Syntax
+
+ The general syntax for declaring a hook is as follows.
+
+
+ Property hooks (full version)
+
+modified) {
+ return $this->foo . ' (modified)';
+ }
+ return $this->foo;
+ }
+ set(string $value) {
+ $this->foo = strtolower($value);
+ $this->modified = true;
+ }
+ }
+}
+
+$example = new Example();
+$example->foo = 'changed';
+print $example->foo;
+?>
+]]>
+
+
+
+ The foo property ends in {}, rather than a semicolon.
+ That indicates the presence of hooks. Both a get and set
+ hook are defined, although it is allowed to define only one or the other. Both hooks have a body,
+ denoted by {}, that may contain arbitrary code.
+
+
+ The set hook additionally allows specifying the type and name of an incoming value,
+ using the same syntax as a method. The type must be either the same as the type of the property,
+ or contravariant (wider) to it.
+ For instance, a property of type string could
+ have a set hook that accepts stringStringable,
+ but not one that only accepts array.
+
+
+ At least one of the hooks references $this->foo, the property itself. That means
+ the property wll be "backed." When calling $example->foo = 'changed',
+ the provided string will be first cast to lowercase, then saved to the backing value.
+ When reading from the property, the previously saved value may conditionally be appended
+ with additional text.
+
+
+ There are a number of short-hand syntax variants as well to handle common cases.
+
+
+ If the get hook is a single expression, then the {}
+ may be omitted and replaced with an arrow expression.
+
+
+ Property get expression
+
+ This example is equivalent to the previous.
+
+
+ $this->foo . ($this->modified ? ' (modified)' : '');
+
+ set(string $value) {
+ $this->foo = strtolower($value);
+ $this->modified = true;
+ }
+ }
+}
+?>
+]]>
+
+
+
+ If the set hook's parameter type is the same as the property type (which is typical),
+ it may be omitted. In that case, the value to set is automatically given the name $value.
+
+
+ Property set defaults
+
+ This example is equivalent to the previous.
+
+
+ $this->foo . ($this->modified ? ' (modified)' : '');
+
+ set {
+ $this->foo = strtolower($value);
+ $this->modified = true;
+ }
+ }
+}
+?>
+]]>
+
+
+
+ If the set hook is only setting a modified version of the passed in value, then it may
+ also be simplified to an arrow expression. The value the expression evaluates to will be set on the backing
+ value.
+
+
+ Property set expression
+
+ $this->foo . ($this->modified ? ' (modified)' : '');
+ set => strtolower($value);
+ }
+}
+?>
+]]>
+
+
+
+ This example is not quite equivalent to the previous, as it does not also modify $this->modified.
+ If multiple statements are needed in the set hook body, use the braces version.
+
+
+ A property may implement zero, one, or both hooks as the situation requires. All shorthand versions are mutually-independent.
+ That is, using a short-get with a long-set, or a short-set with an explicit type, or so on is all valid.
+
+
+ On a backed property, omitting a get or set hook means the default read or
+ write behavior will be used.
+
+
+
+ Virtual properties
+
+ Virtual properties are properties that have no backing value. A property is virtual if neither its get
+ nor set hook references the property itself using exact syntax.
+ That is, a property named $foo whose hook contains $this->foo will be backed.
+ But the following is not a backed property, and will error:
+
+
+ Invalid virtual property
+
+$temp; // Doesn't refer to $this->foo, so it doesn't count.
+ }
+ }
+}
+?>
+]]>
+
+
+
+ For virtual properties, if a hook is omitted then that operation does not exist and
+ trying to use it wil produce an error. Virtual properties take up no memory space in an object.
+ Virtual properties are suited for "derived" properties, such as those that are the combination
+ of two other properties.
+
+
+ Virtual property
+
+ $this->h * $this->w;
+ }
+
+ public function __construct(public int $h, public int $w) {}
+}
+
+$s = new Rectangle(4, 5);
+print $s->area; // prints 20
+$s->area = 30; // Error, as there is no set operation defined.
+?>
+]]>
+
+
+
+ Defining both a get and set hook on a virtual property is also allowed.
+
+
+
+ Scoping
+
+ All hooks operate in the scope of the object being modified.
+ That means they have access to all public, private, or protected methods of the object, as well as any public,
+ private, or protected properties, including properties that may have their own property hooks.
+ Accessing another property from within a hook does not bypass the hooks defined on that property.
+
+
+ The most notable implication of this is that non-trivial hooks may sub-call to an
+ arbitrarily complex method if they wish.
+
+
+ Calling a method from a hook
+
+ $this->sanitizePhone($value);
+ }
+
+ private function sanitizePhone(string $value): string {
+ $value = ltrim($value, '+');
+ $value = ltrim($value, '1');
+
+ if (!preg_match('/\d\d\d\-\d\d\d\-\d\d\d\d/', $value)) {
+ throw new \InvalidArgumentException();
+ }
+ return $value;
+ }
+}
+?>
+]]>
+
+
+
+
+ References
+
+ Because the presence of hooks intercept the read and write process for properties,
+ they cause issues when acquiring a reference to a property or with indirect
+ modification, such as $this->arrayProp['key'] = 'value';.
+ That is because any attempted modification of the value by reference would bypass a set hook, if one is defined.
+
+
+ In the rare case that getting a reference to a property that has hooks defined is necessary, the get
+ hook may be prefixed with & to cause it to return by reference. Defining both get
+ and &get on the same property is a syntax error.
+
+
+ Defining both &get and set hooks on a backed property is not allowed.
+ As noted above, writing to the value returned by reference would bypass the set hook.
+ On virtual properties, there is no necessary common value shared between the two hooks, so defining both is allowed.
+
+
+ Writing to an index of an array property also involves an implicit reference. For that reason, writing to a backed array property
+ with hooks defined is allowed if and only if it defines only a &get hook.
+ On a virtual property, writing to the array returned from either get or &get
+ is legal, but whether that has any impact on the object depends on the hook implementation.
+
+
+ Overwriting the entire array property is fine, and behaves the same as any other property. Only working with
+ elements of the array require special care.
+
+
+
+ Inheritance
+
+ Final hooks
+
+ Hooks may also be declared final,
+ in which case they may not be overridden.
+
+
+ Final hooks
+
+ strtolower($value);
+ }
+}
+
+class Manager extends User
+{
+ public string $username {
+ // This is allowed
+ get => strtoupper($this->username);
+
+ // But this is NOT allowed, because set is final in the parent.
+ set => strtoupper($value);
+ }
+}
+?>
+]]>
+
+
+
+ A property may also be declared final.
+ A final property may not be redeclared by a child class in any way, which precludes
+ altering hooks or widening its access.
+
+
+ Declaring hooks final on a property that is declared final is redundant,
+ and will be silently ignored. This is the same behavior as final methods.
+
+
+ A child class may define or redefine individual hooks on a property by redefining the property
+ and just the hooks it wishes to override. A child class may also add hooks to a property that had none.
+ This is essentially the same as if the hooks were methods.
+
+
+ Hook inheritance
+
+x = $value;
+ }
+ }
+}
+?>
+]]>
+
+
+
+ Each hook overrides parent implementations independently of each other.
+ If a child class adds hooks, any default value set on the property is removed, and must be redeclared.
+ That is the same consistent with how inheritance works on hook-less properties.
+
+
+
+ Accessing parent hooks
+
+ A hook in a child class may access the parent class's property using the parent::$prop keyword,
+ followed by the desired hook. For example, parent::$propName::get().
+ It may be read as “access the prop defined on the parent class,
+ and then run its get operation” (or set operation, as appropriate).
+
+
+ If not accessed this way, the parent class's hook is ignored. This behavior is consistent with how all methods work.
+ This also offers a way to access the parent class's storage, if any.
+ If there is no hook on the parent property, its default get/set behavior will be used.
+ Hooks may not access any other hook except their own parent on their own property.
+
+
+ The example above could be rewritten more efficiently as follows.
+
+
+ Parent hook access (set)
+
+x = $value;
+ }
+ }
+}
+?>
+]]>
+
+
+
+ An example of overriding only a get hook could be:
+
+
+ Parent hook access (get)
+
+ $this->uppercase
+ ? strtoupper(parent::$val::get())
+ : strtolower(parent::$val::get());
+ }
+}
+?>
+]]>
+
+
+
+
+
+ Serialization
+
+ PHP has a number of different ways in which an object may be serialized,
+ either for public consumption or for debugging purposes. The behavior of hooks varies
+ depending on the use case. In some cases, the raw backing value of a property will
+ be used, bypassing any hooks. In others, the property will be read or written "through"
+ the hook, just like any other normal read/write action.
+
+
+ var_dump(): Use raw value
+ serialize(): Use raw value
+ unserialize(): Use raw value
+ __serialize()/__unserialize(): Custom logic, uses get/set hook
+ Array casting: Use raw value
+ var_export(): Use get hook
+ json_encode(): Use get hook
+ JsonSerializable: Custom logic, uses get hook
+ get_object_vars(): Use get hook
+ get_mangled_object_vars(): Use raw value
+
+
+
+
diff --git a/language/oop5/variance.xml b/language/oop5/variance.xml
index b4664c9ca762..cd6142f46c9b 100644
--- a/language/oop5/variance.xml
+++ b/language/oop5/variance.xml
@@ -10,7 +10,7 @@
Covariance allows a child's method to return a more specific type than the return type
- of its parent's method. Whereas, contravariance allows a parameter type to be less
+ of its parent's method. Contravariance allows a parameter type to be less
specific in a child method, than that of its parent.
@@ -242,6 +242,58 @@ Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an i
+
+ Property variance
+
+ By default, properties are neither covariant nor contravariant, hence invariant. That is, their type may not
+ change in a child class at all. The reason for that is "get" operations must be covariant,
+ and "set" operations must be contravariant. The only way for a property to satisfy both requirements is to be invariant.
+
+
+ As of PHP 8.4.0, with the addition of abstract properties (on an interface or abstract class) and
+ virtual properties,
+ it is possible to declare a property that has only a get or set operation.
+ As a result, abstract properties or virtual properties that have only a "get" operation required may be covariant.
+ Similarly, an abstract property or virtual property that has only a "set" operation required may be contravariant.
+
+
+ Once a property has both a get and set operation, however, it is no longer covariant or contravariant
+ for further extension. That is, it is now invariant.
+
+
+ Property type variance
+
+
+]]>
+
+
+