0

PHP 8.4 — Property hooks (get/set on properties)

Advanced5 min read·php-09-022
interviewcompare

Concept

PHP 8.4 introduced property hooks — a mechanism to attach get and set logic directly to a property declaration, eliminating the boilerplate getter/setter pattern. Property hooks allow you to intercept reads and writes to a property using clean, embedded syntax rather than routing through explicit methods.

A property with a get hook becomes a virtual property whose value is computed on access. A property with a set hook intercepts assignment and can validate, transform, or side-effect before storing. You can use either hook independently or combine both. A property with only a get hook is effectively read-only from the outside (no public set). A property with only a set hook validates on write but reads the backing field directly.

The backing field — the actual storage — uses the same property name prefixed with $this->. Inside a hook, $value refers to the incoming value on set, and $this->propName refers to the backing field. Circular hook invocations (a hook that reads the property triggering the same hook) cause a fatal error, so PHP resolves this with the $this->propName syntax that bypasses the hook and accesses the underlying storage slot.

Property hooks interact with readonly in a carefully defined way. A readonly property may have a get hook but not a set hook, because set implies reusability. The hooks are also compatible with interfaces — an interface can declare a property with { get; } or { get; set; } to require that implementors provide those access semantics.

ApproachLines for validated propertyIDE supportSerialization
Public property1GoodDirect
Getter + setter methods6–12GoodRequires mapping
PHP 8.4 property hooks1–5Good (evolving)Accesses backing field
__get/__set magicN (catch-all)PoorComplex

Code Example

php
<?php
declare(strict_types=1);

class User
{
    public string $email {
        get => $this->email;
        set {
            $normalized = strtolower(trim($value));
            if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
                throw new \InvalidArgumentException("Invalid email: {$value}");
            }
            $this->email = $normalized;
        }
    }

    // Computed property — no backing field, derived from other state
    public string $displayName {
        get => trim("{$this->firstName} {$this->lastName}");
    }

    // Set hook that derives another property on write
    public int $age {
        get => $this->age;
        set {
            if ($value < 0 || $value > 150) {
                throw new \RangeException("Age must be 0–150, got {$value}");
            }
            $this->age = $value;
        }
    }

    public function __construct(
        public string $firstName,
        public string $lastName,
        int $age,
        string $email,
    ) {
        $this->age   = $age;   // triggers set hook
        $this->email = $email; // triggers set hook
    }
}

$user = new User('Alice', 'Smith', 30, '  Alice@Example.COM  ');

echo $user->email;       // alice@example.com (normalized)
echo $user->displayName; // Alice Smith (computed)
echo $user->age;         // 30

$user->email = 'BOB@example.com';
echo $user->email;       // bob@example.com

// Property hooks in interfaces
interface HasEmail
{
    public string $email { get; set; }
}

interface ReadableEmail
{
    public string $email { get; }
}

// Readonly + get hook (no set hook allowed with readonly)
class ImmutablePoint
{
    public readonly float $distanceFromOrigin {
        get => sqrt($this->x ** 2 + $this->y ** 2);
    }

    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {}
}

$point = new ImmutablePoint(3.0, 4.0);
echo $point->distanceFromOrigin; // 5.0
// $point->x = 1.0; // Error: readonly

// Asymmetric hook — only set with validation, get reads backing field directly
class Temperature
{
    public float $celsius {
        set {
            if ($value < -273.15) {
                throw new \RangeException("Below absolute zero");
            }
            $this->celsius = $value;
        }
    }

    public function __construct(float $celsius)
    {
        $this->celsius = $celsius;
    }

    public function toFahrenheit(): float
    {
        return $this->celsius * 9 / 5 + 32;
    }
}

$temp = new Temperature(100.0);
echo $temp->celsius;          // 100
echo $temp->toFahrenheit();   // 212

Interview Q&A

Q: How do PHP 8.4 property hooks differ from __get/__set magic methods, and when should you choose each?

Magic methods __get and __set are catch-all interceptors that fire for any undefined or inaccessible property access on an object. They have no type information, are opaque to IDEs and static analyzers, and introduce significant performance overhead because every property access must check whether the property exists before deciding to invoke the magic method. Property hooks are per-property declarations — they are part of the property's definition, carry full type information, are visible to static analyzers, and are resolved at compile time with no runtime property-existence check. Choose property hooks for validated, transformed, or computed properties on known fields. Reserve __get/__set only for dynamic property containers like DTOs that map arbitrary keys to storage.


Q: What is the "backing field" in property hooks and how do you avoid infinite recursion when accessing it?

The backing field is the actual storage slot for a hooked property — the memory location where PHP stores the value. Inside a hook, accessing $this->propName directly refers to the backing field and bypasses the hook, preventing recursion. This is syntactically identical to normal property access but PHP resolves it differently inside the hook context. If a get hook wrote return $this->propName; and propName re-triggered the same get hook, PHP would detect the circular read and throw a fatal error. The engine avoids this by recognizing that inside a hook body, $this->propertyName always means "the storage slot, not the hook chain."


Q: Can property hooks be declared in interfaces, and what do implementations need to provide?

Yes — PHP 8.4 allows interface property declarations with hook requirements: public string $email { get; set; } requires that any implementing class provides both get and set semantics for $email. { get; } alone means the property must be publicly readable but does not constrain writability. This is a stronger contract than methods, because it specifies not just that a method named getEmail() exists, but that the class exposes $email as a readable property — which is how callers naturally access it. Implementations can satisfy the requirement with a plain property (if no hooks are needed), a hooked property, or a computed property with a get hook. This design enables interface-driven contracts around data access patterns rather than method naming conventions.