PHP Property Hooks: Computed and Validated Properties in PHP 8.4

PHP 8.4 introduces property hooksget and set hooks that run when a property is read or written, without requiring explicit getter/setter methods. They bring computed properties (a property whose value is derived from other state) and validated properties (a property that enforces a constraint on write) into the language as first-class syntax, making value objects far more expressive.

Problem: PHP class properties that need computed or validated values — a price that must be positive, a slug that must be lowercase — require explicit getter and setter methods, creating boilerplate that obscures the intent of the value object.

Solution: Use PHP 8.4 Property Hooks to define get and set logic directly on the property declaration: public string $slug { set { $this->slug = strtolower($value); } }. The hook runs automatically on assignment, keeping validation co-located with the property. Interfaces can declare property hooks to enforce computed property contracts.


The examples below show get-only hooks (computed properties), set hooks with validation, combined get+set hooks for format normalisation, and a practical WordPress price value object using property hooks.


 M_PI * $this->radius ** 2;
    }

    public float $circumference {
        get => 2 * M_PI * $this->radius;
    }
}

$c = new Circle();
$c->radius = 5.0;
echo $c->area;           // 78.539...  (computed, not stored)
echo $c->circumference;  // 31.415...

// ── 2. Set hook: validated property ──────────────────────────────────────
class Post {
    public string $title {
        set {
            if ( strlen( $value ) < 3 ) {
                throw new \ValueError( 'Title must be at least 3 characters.' );
            }
            $this->title = sanitize_text_field( $value );  // write to backing storage
        }
    }

    public int $status {
        set {
            $allowed = [ 0, 1, 2 ];
            if ( ! in_array( $value, $allowed, true ) ) {
                throw new \ValueError( "Invalid status: $value" );
            }
            $this->status = $value;
        }
    }
}

$post = new Post();
$post->title  = '  Hello World  ';  // set hook sanitises: stored as "Hello World"
$post->status = 1;
// $post->status = 99;  // throws ValueError

// ── 3. Combined get + set: format normalisation ───────────────────────────
class Email {
    public string $address {
        get => strtolower( $this->address );          // always return lowercase
        set => filter_var( $value, FILTER_VALIDATE_EMAIL )
               ? strtolower( $value )
               : throw new \ValueError( "Invalid email: $value" );
    }
}

// ── 4. WordPress product price with hooks ────────────────────────────────
class WPProduct {
    public float $price {
        set {
            if ( $value < 0 ) {
                throw new \ValueError( 'Price cannot be negative.' );
            }
            $this->price = round( $value, 2 );
        }
        get => $this->price;
    }

    public string $formattedPrice {
        get => '$' . number_format( $this->price, 2 );
    }

    public function __construct(
        public readonly int    $id,
        public readonly string $name,
        float $price,
    ) {
        $this->price = $price;  // triggers the set hook
    }
}


NOTE: A property with only a get hook and no set hook is effectively read-only from outside the class but can be set in the constructor using $this->prop = value — inside the class the backing storage is always writable; to make a property truly immutable outside the constructor, combine a get hook with private(set) visibility.