Property hooks and asymmetric visibility

PHP · Advanced · 14 min

These 8.4 features remove most getter and setter boilerplate while keeping encapsulation.

Property hooks (PHP 8.4) attach get and set logic directly to a property, so callers use plain property syntax while the class keeps control:

public string $email {
    set(string $value) { $this->email = strtolower(trim($value)); }
}
  • A set hook runs on every write, including in the constructor. Inside it, $this->email refers to the backing value.
  • A get hook computes the value on read. A property whose hooks never touch the backing value is virtual: it takes no memory, like public string $fullName { get => "$this->first $this->last"; }.
  • The short form set => strtolower($value); assigns the expression to the backing value.
  • Interfaces and abstract classes can declare properties with hooks, e.g. public string $name { get; }, so a contract can require a readable property.

Asymmetric visibility lets reading and writing have different visibility: public private(set) int $balance = 0; is readable everywhere but writable only inside the class. protected(set) allows subclasses to write. Unlike readonly, the class can change the value as often as it likes.

Rules of thumb: use readonly for true value objects, private(set) for entities whose state changes through methods, and hooks for normalisation or derived values. Hooks cannot be combined with readonly.

Example

<?php // PHP 8.4
final class Account
{
    public private(set) int $balance = 0;

    public string $owner {
        set(string $value) {
            if ($value === '') throw new ValueError('owner required');
            $this->owner = ucwords(strtolower($value));
        }
    }

    public string $label {
        get => "{$this->owner} ({$this->balance})";   // virtual property
    }

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

    public function deposit(int $amount): void { $this->balance += $amount; }
}

$a = new Account('ADA lovelace');
$a->deposit(50);
echo $a->label, "\n";          // Ada Lovelace (50)
try { $a->balance = 1000; } catch (Error $e) { echo "read-only from outside\n"; }

Remember

  • set hooks normalise or validate on every write; get hooks compute on read
  • A property whose hooks never use the backing value is virtual
  • public private(set) = readable everywhere, writable only inside the class

Watch out for

  • Reaching for hooks on a readonly property; the two can't be combined.

Check yourself

  1. What is true of public private(set) int $balance?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.php.net/manual/en/language.oop5.property-hooks.php