From 4ac0873c95507eb482e7402d5e98bc6d873797a6 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 17 Nov 2024 23:19:20 -0600 Subject: [PATCH 01/25] Add basic Hooks syntax. --- language/oop5.xml | 1 + language/oop5/property-hooks.xml | 203 +++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 language/oop5/property-hooks.xml 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/property-hooks.xml b/language/oop5/property-hooks.xml new file mode 100644 index 000000000000..dc0ccb70d521 --- /dev/null +++ b/language/oop5/property-hooks.xml @@ -0,0 +1,203 @@ + + + + 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: + + + One, 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. + + + Two, 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. + + + 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 string|Stringable, + 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->foo . ($this->modified ? ' (modified)' : ''); + + set(string $value) { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} +?> +]]> + + + + + This example is equivalent to the previous. + + + 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->foo . ($this->modified ? ' (modified)' : ''); + + set { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} +?> +]]> + + + + + This example is equivalent to the previous. + + + 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. + + + + + From 29720d0c188bffc975c1a9deccd0e4be338e6b37 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 17 Nov 2024 23:26:08 -0600 Subject: [PATCH 02/25] Document the __PROPERTY__ magic constant. --- language/constants.xml | 6 ++++++ 1 file changed, 6 insertions(+) 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__ From e446eff0d5e96c4fb6c67072f7575f017e234fdd Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 17 Nov 2024 23:40:29 -0600 Subject: [PATCH 03/25] Virtual properties. --- language/oop5/property-hooks.xml | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/language/oop5/property-hooks.xml b/language/oop5/property-hooks.xml index dc0ccb70d521..75eb8b27e231 100644 --- a/language/oop5/property-hooks.xml +++ b/language/oop5/property-hooks.xml @@ -178,7 +178,113 @@ class Example 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; + } +} +?> +]]> + + + +