Property Hooks in PHP 8.4
Marcos Marcolin • April 15, 2024 • 3 min read
php phprfc php84 opensourceProperty Hooks
For a while now, the community has been discussing the implementation of Property Hooks.
A consensus has now been reached, and the feature will be available in PHP 8.4.
This feature was inspired by other languages, such as C#, Kotlin, and Swift.
This article is a summary of the RFC created by Ilija Tovilo and Larry Garfield. The link to the full RFC is available at the end of this post.
I've highlighted the most interesting points for now, but I might write another article covering what was left out.
Motivation
When we need to encapsulate a class property, we usually resort to creating a getter and setter, right?
If that was your first thought, we're on the same page.
For example, if we have a User class with the $name attribute, it's common to create getName() and setName() methods to access and modify that property.
So the example mentioned above would be represented like this:
class User {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->setName = $name
}
}
In PHP 8.0, we can simplify this even further by using Constructor Property Promotion, which lets us define the property directly in the constructor arguments.
class User {
public function __construct(public string $name) {
$this->name = $name;
}
}
Keep in mind that the attribute is public; if it needs to be private, there's no way to avoid needing a getter and setter.
We could also fall back on the magic methods __set and __get, however, they run for every attribute in the class,
which means we'd need to check which attribute is being written to or read at that moment and, if needed,
apply specific validations. This results in confusing, hard-to-read code.
class User
{
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function __get(string $prop): mixed {
return match ($prop) {
'name' => $this->name,
default => throw new Error("Property $prop not defined."),
};
}
public function __set(string $prop, $value): void {
switch ($prop) {
case 'name':
if (! is_string($value)) {
throw new TypeError('Name must be a string');
}
if (strlen($value) === 0) {
throw new ValueError('Name cannot be empty');
}
$this->name = $value;
break;
default:
throw new Error("Error setting property $prop");
}
}
}
Notice how the code can get confusing? Now imagine if the class had several properties.
Usage
Property Hooks were designed to make it easier to apply additional behavior specifically for a single property.
For example, we can check if $name is empty.
class User
{
public string $name {
set {
if (strlen($value) === 0) {
throw new ValueError("Name cannot be empty");
}
$this->name = $value;
}
get {
return strtoupper($this->name);
}
}
public function __construct(string $name) {
$this->name = $name;
}
}
Currently, the RFC introduces two hooks, get and set, but the design is flexible enough to allow more hooks to be added in the future.
Additionally, the proposed syntax also supports a shorthand form.
class User
{
public string $name {
get => strtoupper($this->name);
}
public function __construct(string $name) {
$this->name = $name;
}
}
Interfaces
This feature can also be used within Interfaces.
interface Named
{
public string $name { get; }
}
class User implements Named
{
public function __construct(public readonly string $name) {}
}
get
The get hook, when defined, replaces PHP's default read behavior.
class User
{
public function __construct(private string $firstName, private string $lastName) {}
public string $fullName {
get {
return $this->firstName . " " . $this->lastName;
}
}
}
$user = new User('Marcos', 'Marcolin');
echo $user->fullName; // Marcos Marcolin
Of course, types are validated at runtime, so the hook must return the type defined on the property.
In the example above, $fullName isn't a writable property, so trying to write to it will result in an error.
In the following example, writing is allowed, so we can do:
class User
{
public string $name {
get {
return strtoupper($this->name);
}
}
}
$user = new User();
$user->name = 'marcos'; // The stored value will be 'marcos'
echo $user->name; // The output will be 'MARCOS'
set
The set hook, when defined, replaces PHP's default write behavior.
class User
{
public function __construct(private string $firstName, private string $lastName) {}
public string $fullName {
set (string $value) {
[$this->firstName, $this->lastName)] = explode(' ', $value, 2);
}
}
public function getFirst(): string {
return $this->firstName;
}
}
$user = new User('Marcos', 'Marcolin');
$user->fullName = 'Rasmus Lerdorf';
echo $user->getFirst(); // Rasmus
The hook only accepts a single argument; if specified, it must state the parameter's type and name.
implicit set
If the property's type is already defined, the hook argument's type can be omitted.
public string $fullName {
set (string $value) {
[$this -> first, $this -> last] = explode ('', $value , 2);
}
}
// Omits the type
public string $fullName {
set {
[$this -> first, $this -> last] = explode (' ', $value, 2);
}
}
If the parameter isn't specified, it defaults to $value.
As mentioned earlier for get, the shorthand syntax can also be used for set.
class User {
public string $username {
set(string $value) {
$this->username = strtolower($value);
}
}
// Shorthand syntax
public string $username {
set => strtolower($value);
}
}
Inheritance
A child class can define or redefine individual hooks on a property by redeclaring the property itself and only
the hooks it wants to override. The property's type and visibility are subject to their own rules,
independent of this RFC.
Additionally, a child class can add hooks to a property that doesn't have any.
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('X cannot be negative.');
}
$this->x = $value;
}
}
}
Each hook overrides the parent implementations independently of one another.
Final hooks
Hooks can also be declared final, in which case they cannot be overridden.
class User
{
public string $username {
final set => strtolower($value);
}
}
class Manager extends User
{
public string $username {
// This is allowed
get => strtoupper($this->username);
// This is not allowed
set => strtoupper($value);
}
}
Well, I'll stop here. :)
I hope you enjoyed this brief introduction to Property Hooks.
You can read more about the proposal in the official post: click here.
See you next time, and cheers! 🐘