PHP 8.4 Property Hooks: Kill Your Getters and Setters

PHP 8.4 Property Hooks: Kill Your Getters and Setters

The Boilerplate Problem

Every PHP developer has written this pattern hundreds of times:

<?php

class User {
    private string $email;

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

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

A private property, a getter that just returns it, and a setter that validates and normalizes. Three methods, fifteen lines, and most of it is ceremony. Multiply this by every property on every model in your application, and you’re swimming in boilerplate.

PHP 8.4 property hooks let you attach get and set logic directly to the property declaration. The result is the same behavior with a fraction of the code.

The New Syntax

<?php

class User {
    public string $email {
        set(string $value) {
            if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException('Invalid email');
            }
            $this->email = strtolower($value);
        }
    }
}

$user = new User();
$user->email = '[email protected]'; // Triggers the set hook
echo $user->email; // '[email protected]'

The property is public, so external code reads and writes it directly. But the set hook intercepts writes and applies validation and normalization. No separate getter needed — reading $user->email returns the stored value directly since there’s no get hook.

The incoming value is available as $value inside the set hook. You assign to $this->email to store it. If you want to reject the value, throw an exception.

Short Arrow Syntax

For simple transformations, you can use the arrow syntax:

<?php

class Product {
    public string $name {
        set(string $value) => trim($value);
    }

    public string $slug {
        set(string $value) => strtolower(preg_replace('/[^a-z0-9]+/i', '-', $value));
    }
}

The arrow set syntax implicitly assigns the expression result to the property. You don’t need to write $this->slug = ... — it’s done for you.

Virtual Properties

A property with a get hook but no backing storage is a virtual property. It doesn’t store a value — it computes one every time it’s read.

<?php

class Rectangle {
    public function __construct(
        public float $width,
        public float $height
    ) {}

    public float $area {
        get => $this->width * $this->height;
    }

    public float $perimeter {
        get => 2 * ($this->width + $this->height);
    }
}

$rect = new Rectangle(10, 5);
echo $rect->area;      // 50
echo $rect->perimeter; // 30

$rect->width = 20;
echo $rect->area;      // 100 — recalculated automatically

Virtual properties look like regular properties to the outside world. You read them with $rect->area, not $rect->getArea(). They can’t be written to — attempting $rect->area = 200 throws an error because there’s no set hook and no backing storage.

This is the perfect replacement for those getFullName(), getTotal(), getDisplayLabel() methods that just concatenate or compute from other properties.

Full get and set Hooks

You can define both hooks on the same property for complete control:

<?php

class Temperature {
    private float $celsius;

    public float $fahrenheit {
        get => ($this->celsius * 9 / 5) + 32;
        set(float $value) {
            $this->celsius = ($value - 32) * 5 / 9;
        }
    }

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

$temp = new Temperature(100);
echo $temp->fahrenheit; // 212

$temp->fahrenheit = 32;
echo $temp->fahrenheit; // 32 (stored as 0°C internally)

Here, $fahrenheit is a virtual property backed by $celsius. Writing to it converts to Celsius. Reading from it converts back to Fahrenheit. The external API is in Fahrenheit, but internal storage is in Celsius.

Property Hooks With Constructor Promotion

Property hooks work with constructor promotion, making DTOs and value objects incredibly concise:

<?php

class CreateUserDTO {
    public function __construct(
        public string $name {
            set(string $value) => trim($value);
        },
        public string $email {
            set(string $value) {
                if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    throw new InvalidArgumentException("Invalid email: $value");
                }
                $this->email = strtolower($value);
            }
        },
        public int $age {
            set(int $value) {
                if ($value  150) {
                    throw new RangeException("Age must be between 0 and 150");
                }
                $this->age = $value;
            }
        }
    ) {}
}

$dto = new CreateUserDTO(
    name: '  Alice  ',
    email: '[email protected]',
    age: 30
);

echo $dto->name;  // 'Alice' (trimmed)
echo $dto->email; // '[email protected]' (lowercased)

Validation, normalization, and construction — all in one compact declaration.

Hooks and Interfaces

Interfaces can now require properties with specific hook contracts:

<?php

interface HasDisplayName {
    // Requires a readable string property
    public string $displayName { get; }
}

class User implements HasDisplayName {
    public function __construct(
        private string $firstName,
        private string $lastName
    ) {}

    public string $displayName {
        get => "$this->firstName $this->lastName";
    }
}

This is a significant improvement for interface design. Previously, interfaces could only require methods. Now they can require readable or writable properties, making contracts more expressive.

What You Can’t Do

readonly Properties Can’t Have Hooks

This is an intentional restriction. readonly properties are set once in the constructor and never modified. Adding a set hook would conflict with readonly semantics, so it’s not allowed.

// This is a compile error
public readonly string $name {
    set(string $value) => trim($value);  // Not allowed
}

If you need constructor-time validation on a readonly property, do it in the constructor body.

No Hooks on Static Properties

Static properties don’t support hooks. This is a technical limitation of the current implementation.

Common Mistakes

1. Infinite Recursion in set Hooks

If your set hook assigns to the property using $this->propertyName = ..., it triggers the set hook again, causing infinite recursion. Use the arrow syntax for simple transformations, which handles assignment implicitly.

// WRONG: infinite recursion
public string $name {
    set(string $value) {
        $this->name = ucfirst($value);  // Calls set again!
    }
}

// CORRECT: arrow syntax assigns implicitly
public string $name {
    set(string $value) => ucfirst($value);
}

2. Heavy Computation in get Hooks

Virtual properties with get hooks are computed on every access. If the computation is expensive (database query, file read), consider caching the result in a separate property.

3. Forgetting Type Declarations

Hooks are only available on typed properties. Untyped properties (public $name without a type) can’t have hooks.

Best Practices

  • Use hooks for validation and normalization. Trim whitespace, lowercase emails, validate ranges — these belong in set hooks.
  • Use virtual properties for computed values. Replace getFullName(), getTotal(), and similar methods with virtual properties.
  • Prefer arrow syntax for simple transformations. It’s concise and avoids the recursion trap.
  • Don’t migrate working code unnecessarily. Existing getter/setter methods work fine. Use hooks for new code or when you’re already refactoring a class.
  • Combine with constructor promotion for clean DTOs and value objects.

Wrapping Up

Property hooks are PHP 8.4’s most impactful feature for everyday code. They eliminate getter/setter boilerplate, make properties self-validating, and enable virtual properties that look like regular data access but compute values on the fly.

The best part: external code doesn’t change. Consumers still write $user->email = 'value' and echo $user->area. The hooks are an implementation detail, invisible to the caller. That’s good API design.

FAQ

When was PHP 8.4 released?

PHP 8.4 was released on November 21, 2024. Property hooks are available in all 8.4+ versions.

Can I use hooks with Laravel Eloquent models?

Not directly on Eloquent’s magic properties (which use __get/__set), but you can use them on DTOs, value objects, form requests, and any plain PHP classes in your Laravel application. The Laravel community is actively adopting hooks for request validation and API resource objects.

Do hooks work with serialization?

Serialization uses the property’s stored value, not the get hook’s return value. Virtual properties (no backing storage) are excluded from serialization by default since they have no stored value to serialize.

Can I define hooks in abstract classes?

Yes. Abstract classes can declare abstract properties with hook signatures that child classes must implement, similar to abstract methods.

editor's pick

latest video

news via inbox

Nulla turp dis cursus. Integer liberos  euismod pretium faucibua

Leave A Comment