Two readonly keywords for two properties in a two-property class. That is what an immutable Money looks like in PHP 8.1:

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}
}

PHP 8.2 is at release candidate stage, and the feature I am waiting for is this one. The keyword moves up and says it once:

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
}

Every property is readonly, and the class refuses dynamic properties on top. Cosmetics, yes. But cosmetics that make the right thing the short thing, and that changes what people actually write.

Why I care about immutability here at all. A Money that can be mutated is a bug generator. Some method receives it, “adjusts” the amount for its own calculation, and the caller’s variable changed too, because objects travel by handle. With readonly this mutation is impossible. Want a different amount, construct a new value. The number of states in the system drops, and with it the number of surprises. Same story for command DTOs: what the handler received is exactly what was created, nobody edited it on the way.

Where I will not use it: entities. An ORM entity is mutable by its job description, its whole purpose is tracked change. Doctrine also wants to hydrate properties from outside the constructor, and proxies want to write to them. Readonly fights the tool. Same caution with anything that goes through a serializer that likes to construct empty and fill in.

So the border is clean. Values and commands: readonly class. Entities and anything with a lifecycle: normal class.

I have a Money in production right now with a setter on it. Somebody needed it for an import. December is close.