ZetCode

PHP Property Hooks

last modified October 22, 2026

In this article, we show how to use property hooks in PHP. Property hooks were introduced in PHP 8.4. They let us attach get and set logic directly to a property declaration, so validation, normalization, and computed values no longer require separate getter and setter methods or the __get and __set magic methods.

The examples require PHP 8.4 or newer. A hook can be written as a block (get { ... }) or, for a single expression, with the arrow syntax (get => expression;).

Main Features of PHP Property Hooks

Backed property with a set hook

A property hook is a block of code attached directly to a property declaration. A set hook runs whenever a value is assigned to the property. Inside the hook the incoming value is available as $value.

set_hook.php
<?php

class Product
{
    public string $name {
        set {
            if (strlen($value) < 3) {
                throw new InvalidArgumentException(
                    "Name must be at least 3 characters long.");
            }
            $this->name = ucfirst($value);
        }
    }
}

$product = new Product();
$product->name = "laptop";
echo $product->name, "\n";

try {
    $product->name = "ab";
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";
}

The name property is backed: the hook assigns to $this->name, which refers to the real storage and does not call the hook again. The hook validates the value and capitalizes it before storing it. Note that no __set method and no private field are needed; the property stays public and callers use plain assignment.

$ php set_hook.php
Laptop
Name must be at least 3 characters long.

The get hook

A get hook runs whenever the property is read. When the hook consists of a single expression, we can use the short arrow syntax.

get_hook.php
<?php

class User
{
    public string $email {
        get => strtolower($this->email);
    }

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

$user = new User("John.Doe@Example.COM");
echo $user->email, "\n";

The email property has a get hook but no set hook, so assignments store the value as is. Inside the hook, $this->email reads the stored value directly without recursing. The hook normalizes the e-mail address to lowercase each time it is read, while the stored value remains unchanged.

$ php get_hook.php
john.doe@example.com

Virtual properties

A property that has hooks but never reads or writes $this->prop inside them is virtual: it has no storage. Virtual properties are computed from other state.

virtual.php
<?php

class Person
{
    public function __construct(
        public string $firstName,
        public string $lastName,
    ) {}

    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
        set {
            [$this->firstName, $this->lastName] = explode(' ', $value, 2);
        }
    }
}

$p = new Person("John", "Doe");
echo $p->fullName, "\n";

$p->fullName = "Jane Roe";
echo $p->firstName, "\n";
echo $p->lastName, "\n";

The fullName property is virtual. Its get hook builds the value from two real properties, and its set hook splits an assigned string back into them. A virtual property that has only a get hook is read-only; assigning to it raises an Error.

$ php virtual.php
John Doe
Jane
Roe

Constructor property promotion with hooks

Hooks can be combined with constructor promotion. The hooks are declared in a block after the parameter.

promotion.php
<?php

class Account
{
    public function __construct(
        public float $balance = 0.0 {
            set {
                if ($value < 0) {
                    throw new RangeException("Balance cannot be negative.");
                }
                $this->balance = round($value, 2);
            }
        },
    ) {}
}

$account = new Account(100);
echo $account->balance, "\n";

$account->balance = 99.567;
echo $account->balance, "\n";

try {
    $account->balance = -5;
} catch (RangeException $e) {
    echo $e->getMessage(), "\n";
}

The hook is attached to the promoted balance parameter. It is applied to the value passed to the constructor and to later assignments. Here it rejects negative values and rounds the balance to two decimal places.

$ php promotion.php
100
99.57
Balance cannot be negative.

Logging property changes

Hooks make it easy to observe a property without changing the code that uses it.

logging.php
<?php

class Settings
{
    public string $theme = 'light' {
        set {
            echo "Theme changed from {$this->theme} to $value\n";
            $this->theme = $value;
        }
    }
}

$settings = new Settings();
$settings->theme = 'dark';
$settings->theme = 'solarized';
echo $settings->theme, "\n";

Every assignment to theme goes through the set hook, which prints the old and new values before storing the new one. The initial value in the declaration does not trigger the hook. Reading the property uses the default behaviour because no get hook is defined.

$ php logging.php
Theme changed from light to dark
Theme changed from dark to solarized
solarized

Interfaces and abstract properties

Interfaces and abstract classes can declare properties together with the hooks that implementations must support.

interface.php
<?php

interface Named
{
    public string $name { get; }
}

class Dog implements Named
{
    public function __construct(public string $name) {}
}

class Robot implements Named
{
    public string $name {
        get => 'RX-' . $this->id;
    }

    public function __construct(private int $id) {}
}

function greet(Named $n): void
{
    echo "Hello, {$n->name}\n";
}

greet(new Dog("Rex"));
greet(new Robot(78));

The Named interface requires a readable name property. Dog satisfies it with an ordinary public property, while Robot provides a virtual property computed by a get hook. Callers do not care which implementation they receive.

$ php interface.php
Hello, Rex
Hello, RX-78

Asymmetric visibility

PHP 8.4 also introduces asymmetric visibility, which complements hooks: a property can be publicly readable but writable only from inside the class.

asymmetric.php
<?php

class Counter
{
    public private(set) int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
}

$c = new Counter();
$c->increment();
$c->increment();
echo $c->count, "\n";

try {
    $c->count = 10;
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}

The private(set) modifier restricts writes to the class scope while reads stay public. This is often a simpler alternative to a hook when we only need to prevent outside modification.

$ php asymmetric.php
2
Cannot modify private(set) property Counter::$count from global scope

Limitations

Source

PHP Property Hooks - Documentation

In this article, we have shown how to use property hooks in PHP to define custom behavior for property access and modification. Property hooks are a powerful tool for enforcing validation, logging, and encapsulating property logic.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all PHP tutorials.