Skip to content

Instantly share code, notes, and snippets.

@iluuu1994
Last active January 25, 2023 13:44
Show Gist options
  • Save iluuu1994/4ca8cc52186f6039f4b90a0e27e59180 to your computer and use it in GitHub Desktop.
Save iluuu1994/4ca8cc52186f6039f4b90a0e27e59180 to your computer and use it in GitHub Desktop.
Property Hooks

Property Hooks

Use case 0: Stored property

No change necessary, can be overridden with hooks without changes.

class C {
    public int $prop;
}

Use case 1: Computed property

Gets called anytime the property is accessed. Changes to the property are forbidden, via reference or otherwise.

class C {
    public int $prop {
        get {
            static $i = 0;
            return $i++;
        }
    }
}

Use case 2: Virtual properties

The property can be accessed and mutated, although not via reference or indirectly (i.e. through $c->prop['foo']['bar'] = 'bar';). This is usually useful when wrapping a property and using it to store the actual value.

class C {
    public array $accessedProperties = [];
    public string $_prop;
    public string $prop {
        get {
            $this->accessedProperties[] = 'prop';
            return $_prop;
        }
        set {
            // $value is guaranteed to be a string here
            $this->_prop = strtoupper($value);
        }
    }
}

Use case 3: Observing accesses and mutations

The property still stores its own value but calls the {before,after}Set methods to avoid he need to create property wrappers for simple cases. The same caveat applies, the property must not contain references and can't be modified indirectly. beforeSet allows transforming and returning a value before it is stored.

class C {
    public string $prop {
        beforeSet {
            return strtoupper($value);
        }
    }
}

Use case 4: Overriding a property to add behavior

Sometimes a sub class wants to add behavior to a property, like modifying the value before it is stored, or adding validation. Overriding properties by adding hooks is allowed. The parent property can be accessed through parent::$prop. This will either call the parent {get,set} hook or access the native property, depending on what the parent has declared. The reference and indirect modification caveat once again applies. For this reason, this is not a good approach for arrays.

class C {
    public string $prop;
}
class D extends C {
    public string $prop {
        beforeSet {
            return strtoupper($value);
        }
    }
}

Use case 5: Arrays properties

As mentioned previously, because hooks disallow indirect modification and references and that's the primary way to modify arrays it is not practical to create array properties with hooks. In general, we don't recommend making array properties publicly writable at all. The reason for that is that arrays can be modified in many ways with little control over it, as PHP does not support typed arrays. Instead, we believe that is is preferable to only make the array accessible in public but provide dedicated methods in the given class to make specific changes to the array. This requires a little more typing but avoids the need to do sanitation of C::$names when used. Furthermore, overriding a modification of C::$names becomes as easy as overriding the given modifier method.

class C {
    /** @var list<string> $_names */
    private array $_names;
    /** @var list<string> $names */
    public array $names { get => $this->_names; }

    public function addName(string $name) {
        $ths->_names[] = $name;
    }
}

Some thoughts on this approach:
In the previous proposal, only properties that enabled hooks could be overridden. This was achieved through the public string $prop {} syntax. The rationale was that adding hooks to a property would constitute a BC break because they disable references and indirect modification. However, there are a couple of downsides to this:

  • Every property would need to modified with {} to make adding hooks possible, all to avoid BC breaks when adding the hooks due to disabling indirect modification and references. For non-array properties this is mostly irrelevant as indirect modification is not possible and interaction with references is rare.
  • The difference between public string $foo; and public string $foo {} is not immediately obvious and has proven confusing to many people.
  • It adds further complexity to both the language and implementation.

While this approach could be considered unsound by potentially breaking references and indirect modification in polymorphic code that assumes dealing with the parent class, we think that risk is not worth the benefits of adding it. Furthermore, there are two possible mitigation strategies that could be implemented:

  • Error when creating an array-typed property with hooks
    • Advantage: Cheap and easy to implement
    • Disadvantage: Will only catch properties that are typed with array, can be circumvented with mixed
  • Throw when storing any array in a property with hooks to immediately hint at the fact that this might not work as you'd expect
    • Advantage: Will work for any property with hooks, no matter what the type is
    • Disadvantage: Only works at runtime and will come at a performance cost

Either way, this should be a rare occurrence. Non-array properties cannot be modified indirectly and rarely interact with references. For native array properties we don't recommend exposing them publicly in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment