P
PHP 8.4 Property Hooks
PHPPHP 8.4Backend
PHP 8.4 Property Hooks: Getters and Setters Reimagined
Master PHP 8.4 property hooks. Learn get/set hooks, asymmetric visibility, virtual properties, and how they replace traditional getters and setters.

Hoceine El Idrissi
Full Stack Developer
9 min read
PHP 8.4 Property Hooks: Getters and Setters Reimagined
PHP 8.4 introduces property hooks, a game-changing feature that lets you define get and set logic directly on properties. Say goodbye to boilerplate getter and setter methods.
The Problem with Traditional Getters/Setters
php
// Before PHP 8.4: Verbose and boilerplate-heavy
class User
{
private string $firstName;
private string $lastName;
public function getFirstName(): string
{
return $this->firstName;
}
public function setFirstName(string $value): void
{
$this->firstName = trim($value);
}
public function getLastName(): string
{
return $this->lastName;
}
public function setLastName(string $value): void
{
$this->lastName = trim($value);
}
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
}
Property Hooks Syntax
php
// PHP 8.4: Clean and expressive
class User
{
public string $firstName {
set => trim($value);
}
public string $lastName {
set => trim($value);
}
public string $fullName {
get => $this->firstName . ' ' . $this->lastName;
}
}
$user = new User();
$user->firstName = ' John '; // Automatically trimmed
$user->lastName = ' Doe '; // Automatically trimmed
echo $user->fullName; // "John Doe"
Get Hooks
Basic Get Hook
php
class Product
{
public float $price;
public float $taxRate = 0.20;
public float $priceWithTax {
get => $this->price * (1 + $this->taxRate);
}
}
$product = new Product();
$product->price = 100;
echo $product->priceWithTax; // 120
Multi-Line Get Hook
php
class Order
{
public array $items = [];
public float $total {
get {
$sum = 0;
foreach ($this->items as $item) {
$sum += $item['price'] * $item['quantity'];
}
return $sum;
}
}
}
Set Hooks
Basic Set Hook
php
class Email
{
public string $address {
set => strtolower(trim($value));
}
}
$email = new Email();
$email->address = ' JOHN@EXAMPLE.COM ';
echo $email->address; // "john@example.com"
Set Hook with Validation
php
class User
{
public int $age {
set {
if ($value < 0 || $value > 150) {
throw new InvalidArgumentException('Age must be between 0 and 150');
}
$this->age = $value;
}
}
}
$user = new User();
$user->age = 25; // Works
$user->age = 200; // Throws exception
Set Hook with Type Coercion
php
class Config
{
public string $value {
set(string|int|bool $value) {
$this->value = match(true) {
is_bool($value) => $value ? 'true' : 'false',
is_int($value) => (string) $value,
default => $value,
};
}
}
}
$config = new Config();
$config->value = true; // Stored as "true"
$config->value = 42; // Stored as "42"
Combined Get and Set Hooks
php
class Temperature
{
private float $celsius;
public float $fahrenheit {
get => ($this->celsius * 9/5) + 32;
set {
$this->celsius = ($value - 32) * 5/9;
}
}
public float $celsius {
get => $this->celsius;
set {
if ($value < -273.15) {
throw new InvalidArgumentException('Temperature below absolute zero');
}
$this->celsius = $value;
}
}
}
$temp = new Temperature();
$temp->celsius = 0;
echo $temp->fahrenheit; // 32
$temp->fahrenheit = 212;
echo $temp->celsius; // 100
Virtual Properties
Properties with only a get hook are virtual—they compute values without storing data:
php
class Rectangle
{
public function __construct(
public float $width,
public float $height,
) {}
// Virtual property - no backing storage
public float $area {
get => $this->width * $this->height;
}
public float $perimeter {
get => 2 * ($this->width + $this->height);
}
public bool $isSquare {
get => $this->width === $this->height;
}
}
$rect = new Rectangle(10, 20);
echo $rect->area; // 200
echo $rect->perimeter; // 60
echo $rect->isSquare; // false
Asymmetric Visibility
PHP 8.4 also introduces asymmetric visibility:
php
class User
{
// Public read, private write
public private(set) string $id;
// Public read, protected write
public protected(set) string $name;
public function __construct(string $name)
{
$this->id = uniqid();
$this->name = $name;
}
}
$user = new User('John');
echo $user->id; // Works (public read)
$user->id = '123'; // Error! (private set)
Combined with Hooks
php
class Post
{
public private(set) string $slug {
set => Str::slug($value);
}
public function __construct(
public string $title,
) {
$this->slug = $title;
}
}
$post = new Post('Hello World');
echo $post->slug; // "hello-world"
$post->slug = 'test'; // Error! Cannot set from outside
Constructor Property Promotion with Hooks
php
class Product
{
public function __construct(
public string $name,
public float $price {
set {
if ($value < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $value;
}
},
) {}
}
Interface Compatibility
php
interface HasFullName
{
public string $fullName { get; }
}
class User implements HasFullName
{
public function __construct(
public string $firstName,
public string $lastName,
) {}
public string $fullName {
get => "$this->firstName $this->lastName";
}
}
Abstract Properties
php
abstract class Model
{
abstract public string $tableName { get; }
}
class User extends Model
{
public string $tableName {
get => 'users';
}
}
Real-World Examples
Money Value Object
php
class Money
{
public function __construct(
private int $cents,
) {}
public string $formatted {
get => '$' . number_format($this->cents / 100, 2);
}
public float $dollars {
get => $this->cents / 100;
set {
$this->cents = (int) ($value * 100);
}
}
}
$money = new Money(1999);
echo $money->formatted; // "$19.99"
echo $money->dollars; // 19.99
$money->dollars = 25.50;
echo $money->formatted; // "$25.50"
Entity with Timestamps
php
class Entity
{
public private(set) DateTimeImmutable $createdAt;
public private(set) DateTimeImmutable $updatedAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
public string $createdAtFormatted {
get => $this->createdAt->format('M j, Y');
}
public function touch(): void
{
$this->updatedAt = new DateTimeImmutable();
}
}
Form Input
php
class FormInput
{
public string $value = '' {
set => htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
}
public bool $isEmpty {
get => $this->value === '';
}
}
Migration from Getters/Setters
php
// Before
class User
{
private string $email;
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = strtolower(trim($email));
}
}
// After PHP 8.4
class User
{
public string $email {
set => strtolower(trim($value));
}
}
// Usage remains the same for property access
$user->email = 'TEST@EXAMPLE.COM';
echo $user->email; // "test@example.com"
Best Practices
- Keep hooks simple - Complex logic should be in methods
- Use for computed values - Virtual properties are perfect for derived data
- Validate in setters - Enforce invariants at the property level
- Consider caching - Expensive computations should cache results
php
class Report
{
private ?array $cachedData = null;
public array $data {
get {
return $this->cachedData ??= $this->computeExpensiveData();
}
}
public function invalidateCache(): void
{
$this->cachedData = null;
}
}
Conclusion
PHP 8.4 property hooks eliminate boilerplate while making code more expressive. Combined with asymmetric visibility, you get fine-grained control over property access with minimal syntax.