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 + + +]]> + + +