readonly + accessors

← readonly + accessors

Ashterix

docs: English JsonRpcBundle docs, REST and MCP Bundle sections, package cards

en/articles/from-tg/series/php-can-do/readonly.md new file +103 −0

readonly + accessors

Stable DTOs and Value Objects without boilerplate

  • Added in: readonly — PHP 8.1, accessors — PHP 8.4
  • What it gives you: the ability to create objects whose data can't be changed by accident, plus a new syntax for controlling access to properties via get / set without separate methods.

What it's about

readonly makes a property immutable after initialization — the value can be set only once (usually in the constructor).

accessors let you add control over reading and writing properties without classic getters and setters — right in the class body.

Together they make a perfect combination for DTOs, VOs and configuration classes: the data is immutable and access is controlled.

The classic approach

class Product {
    private string $name;
    private float $price;

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    public function getPrice(): float {
        return $this->price;
    }

    public function setPrice(float $price): void {
        if ($price < 0) {
            throw new InvalidArgumentException('Price must be positive');
        }
        $this->price = $price;
    }
}

It works, but most of this code exists only to provide basic encapsulation.

Now the same — the modern way

class Product {
    public readonly string $name;
    private float $price {
        get => $this->price;
        set => $this->price = max(0, $value);
    }

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
    }
}

readonly guarantees the name is immutable, and the accessors automatically check the price on write.

One more example — a Value Object

class Coordinates {
    public readonly float $lat {
        get => round($this->lat, 6);
    }

    public readonly float $lng {
        get => round($this->lng, 6);
    }

    public function __construct(float $lat, float $lng)
    {
        $this->lat = $lat;
        $this->lng = $lng;
    }
}

No changes after creation, only controlled access on read.

Where it really helps

  • DTO: API requests and responses;
  • Value Object: Money, Price, GeoPoint, Range;
  • Immutable configurations: service parameters, constant states.

Conclusion

readonly and accessors are a step towards real record classes in PHP. They let you write clean, predictable structures without a ton of boilerplate.

Today every DTO of mine is readonly.


First published in Ukrainian in the Telegram channel “Created to die” on November 30, 2025.