Property Hooks

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. Property hooks, added in PHP 8.4, attach code to reading (get) or writing (set) a property, so that a property can validate, normalise or compute its value while still being accessed as $object->property. They remove the main reason for the getter/setter boilerplate that PHP code traditionally carried, and they let interfaces declare properties. Hooks complement the other tools described in Visibility, Readonly and Asymmetric Visibility.

get and set hooks

A hooked property has a body in braces after its declaration, containing a get hook, a set hook, or both. Each hook has a short form (an expression after =>) and a long form (a block):

  • the short get form returns the expression;

  • the short set form assigns the expression’s value to the property;

  • in the long forms, get must return a value, and set assigns explicitly with $this->prop = ….

Inside a set hook, the incoming value is $value (or a name you choose: set(string $title) { … }).

<?php

declare(strict_types=1);

final class Book
{
    // short set: the trimmed value is stored
    public string $title {
        set => trim($value);
    }

    // long set: validate, then store
    public string $isbn {
        set(string $value) {
            $digits = str_replace('-', '', $value);
            if (!preg_match('/^97[89]\d{10}$/', $digits)) {
                throw new InvalidArgumentException("Invalid ISBN: {$value}");
            }
            $this->isbn = $digits;
        }
    }

    // both hooks: store an int, never below zero
    public int $copies = 0 {
        get => $this->copies;
        set => max(0, $value);
    }
}

$book = new Book();
$book->title = '  Kindred ';
$book->isbn = '978-0-00-000001-1';
$book->copies = -3;

var_dump($book->title, $book->isbn, $book->copies);   // "Kindred" "9780000000011" int(0)

try {
    $book->isbn = '12345';
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";                     // Invalid ISBN: 12345
}

Inside its own hooks, $this->title refers to the property’s backing value directly, without calling the hook again; accessing it from any other method goes through the hooks.

Source: Property Hooks

Widening the set type

A set hook may accept a wider type than the property, as long as what it stores matches the property’s type. This is how a property can accept several input forms while always holding one type:

<?php

declare(strict_types=1);

final class Loan
{
    public DateTimeImmutable $due {
        set(DateTimeImmutable|string $value) {
            $this->due = is_string($value) ? new DateTimeImmutable($value) : $value;
        }
    }
}

$loan = new Loan();
$loan->due = '2026-10-26';                     // accepted as a string...
var_dump($loan->due instanceof DateTimeImmutable);   // bool(true) ...stored as a DateTimeImmutable

Source: Property Hooks (set hook parameter type)

Backed and virtual properties

A hooked property is backed if any of its hooks refers to the property itself ($this->title), or if it uses a short set: then it has storage, a default value is allowed, and it appears in var_dump(), serialisation and array casts. A property whose hooks never refer to it is virtual: it has no storage at all and is computed on each access. A virtual property with only a get hook is read-only; writing it throws.

<?php

declare(strict_types=1);

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

    // virtual: computed, no storage
    public string $fullName {
        get => "{$this->firstName} {$this->lastName}";
    }

    // virtual with both hooks: splitting on write
    public string $sortName {
        get => "{$this->lastName}, {$this->firstName}";
        set {
            [$this->lastName, $this->firstName] = array_map(trim(...), explode(',', $value, 2));
        }
    }
}

$a = new Author('Octavia', 'Butler');
echo $a->fullName, ' | ', $a->sortName, "\n";        // Octavia Butler | Butler, Octavia

$a->sortName = 'Le Guin, Ursula K.';
echo $a->fullName, "\n";                             // Ursula K. Le Guin

var_dump((new ReflectionProperty(Author::class, 'fullName'))->isVirtual());   // bool(true)

try {
    $a->fullName = 'x';
} catch (Error $e) {
    echo $e->getMessage(), "\n";                     // Property Author::$fullName is read-only
}

Source: Property Hooks (virtual properties)

Properties in interfaces and abstract classes

Since 8.4, interfaces can declare properties, by specifying which hooks an implementation must support: { get; } requires the property to be readable, { set; } writable, { get; set; } both. Any public property satisfies a get requirement — a plain property, a hooked one, a readonly one or one with asymmetric visibility. Abstract classes can declare abstract properties the same way, with any visibility other than private.

<?php

declare(strict_types=1);

interface Lendable
{
    public string $label { get; }          // implementers must make $label readable
}

abstract class Item implements Lendable
{
    abstract public string $code { get; set; }
}

final class Dvd extends Item
{
    public string $code = '';                         // a plain property satisfies { get; set; }
    public function __construct(public readonly string $title) {}
    public string $label { get => "DVD: {$this->title}"; }
}

final class Ebook implements Lendable
{
    public function __construct(public readonly string $label) {}   // readonly also satisfies { get; }
}

foreach ([new Dvd('Solaris'), new Ebook('Kindred (EPUB)')] as $item) {
    echo $item->label, "\n";
}

This lets interfaces describe data (interface HasIsbn { public string $isbn { get; } }) without forcing a getIsbn() method.

Inheritance, final hooks and the Override attribute

A child class can add or override hooks of an inherited property. It reaches the parent’s hook with parent::$propertyName::get() or parent::$propertyName::set($value). A hook can be declared final to forbid overriding it, and a whole property can be final. Since PHP 8.5, #[\Override] can be placed on a property to assert that it overrides a parent property, just like on methods.

<?php

declare(strict_types=1);

class Shelf
{
    public string $code = '' {
        set => strtoupper($value);
    }

    public string $section {
        final set => ucfirst($value);      // subclasses cannot override this hook
    }
}

final class KidsShelf extends Shelf
{
    #[\Override]                            // 8.5: attribute on properties
    public string $code {
        set {
            parent::$code::set("K-{$value}");   // call the parent's set hook
        }
    }
}

$s = new KidsShelf();
$s->code = 'a12';
$s->section = 'picture books';
echo $s->code, ' / ', $s->section, "\n";      // K-A12 / Picture books

Interactions and restrictions

  • readonly: hooked properties cannot be readonly; use a get-only virtual property, asymmetric visibility or a set hook that refuses a second write.

  • Arrays: modifying an array property in place ($obj->list[] = 1) on a backed property with a set hook throws "Indirect modification … is not allowed", because the hook cannot see the change; assign a whole new array instead. A get hook can be declared &get to return by reference, but then a set hook is not allowed.

  • References: $ref = &$obj->hooked is not allowed when a set hook exists.

  • __PROPERTY__: inside a hook, this magic constant holds the property’s name, useful for shared validation helpers.

  • Static properties cannot have hooks.

  • Constructor promotion: promoted properties can have hooks (written after the parameter).

  • Raw versus hooked access: serialize(), var_dump() and (array) casts read the raw backing values of backed properties, bypassing get hooks (virtual properties, having no storage, are skipped); get_object_vars(), json_encode() and foreach over the object go through the get hooks. Reflection offers getRawValue()/ setRawValue() and isVirtual() for code that needs to bypass or inspect hooks.

<?php

declare(strict_types=1);

final class Playlist
{
    public array $titles = [] {
        set => array_values(array_unique($value));
    }
}

$p = new Playlist();
try {
    $p->titles[] = 'Kindred';                       // in-place modification
} catch (Error $e) {
    echo $e->getMessage(), "\n";                   // Indirect modification of Playlist::$titles is not allowed
}

$p->titles = [...$p->titles, 'Kindred', 'Kindred', 'Dawn'];   // assign a new array instead
print_r($p->titles);                               // ["Kindred", "Dawn"]

final class Label
{
    public string $text = 'kindred' {
        get => ucfirst($this->text);
    }
}

$l = new Label();
var_dump($l->text, get_object_vars($l)['text'], ((array) $l)['text']);   // "Kindred" "Kindred" (hooked) "kindred" (raw)

Replacing getters and setters

Hooks change how PHP classes are designed. A class that used to need four methods per field:

<?php

declare(strict_types=1);

// Before 8.4
final class MemberOld
{
    private string $email;

    public function getEmail(): string { return $this->email; }

    public function setEmail(string $email): void
    {
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Invalid e-mail');
        }
        $this->email = strtolower($email);
    }
}

// Since 8.4
final class Member
{
    public string $email {
        set {
            if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
                throw new InvalidArgumentException('Invalid e-mail');
            }
            $this->email = strtolower($value);
        }
    }
}

$m = new Member();
$m->email = 'Reader@Library.example';
echo $m->email, "\n";               // reader@library.example

The benefit beyond brevity is evolution: a class can start with a plain public string $email, and add validation later as a hook without changing a single caller, which used to require switching every $m->email to $m->getEmail() up front "just in case".

Guidelines:

  • Keep hooks cheap and free of side effects visible to callers (no I/O, no events): property access should feel like property access. Use methods for operations.

  • Prefer readonly or asymmetric visibility when no logic is needed.

  • Do not use hooks for lazy loading heavy resources; lazy objects (8.4, Cloning, Late Static Binding and Object Lifecycle) exist for that.

  • Frameworks are adopting hooks gradually; Doctrine and Laravel’s Eloquent still use their own accessors (see Eloquent Casts, Accessors and Serialization).

Where older books and tutorials are outdated

Every book consulted for this section predates PHP 8.4, so they all teach private properties with getX()/setX() methods, __get()/__set() magic for computed or validated properties (slow, invisible to static analysis), and interfaces that can only require methods. Property hooks (8.4) and #[\Override] on properties (8.5) replace those patterns.