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
getform returns the expression; -
the short
setform assigns the expression’s value to the property; -
in the long forms,
getmustreturna value, andsetassigns 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.
Source: Interface properties and Class Abstraction
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
Source: Property Hooks (inheritance) and RFC: the Override attribute for properties
Interactions and restrictions
-
readonly: hooked properties cannot bereadonly; use aget-only virtual property, asymmetric visibility or asethook that refuses a second write. -
Arrays: modifying an array property in place (
$obj->list[] = 1) on a backed property with asethook throws "Indirect modification … is not allowed", because the hook cannot see the change; assign a whole new array instead. Agethook can be declared&getto return by reference, but then asethook is not allowed. -
References:
$ref = &$obj->hookedis not allowed when asethook 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, bypassinggethooks (virtual properties, having no storage, are skipped);get_object_vars(),json_encode()andforeachover the object go through thegethooks. Reflection offersgetRawValue()/setRawValue()andisVirtual()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)
Source: Property Hooks (limitations) and ReflectionProperty::getRawValue
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
readonlyor 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).
Source: Property Hooks and RFC: Property hooks
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.