PHP8.2のreadonlyクラスでプロパティ全体を不変にする
readonlyクラスとは、クラス宣言そのものにreadonlyを付けることで、中のプロパティ全部を自動的に読み取り専用にできるPHP 8.2の機能です。
PHP 8.1でプロパティ単位のreadonlyが入ったとき、DTOや値オブジェクトのコンストラクタに一つ一つreadonlyを書いていた人は多いと思います。あれを毎回書くのが地味に面倒だったんですが、8.2でクラスごと宣言できるようになりました。
書き方はどう変わる?
プロパティ単位のreadonlyだと、コンストラクタプロモーションのそれぞれにreadonlyを書く必要がありました。
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {
}
}
クラス側にreadonlyを付けると、プロパティごとの記述が要らなくなります。
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
}
}
プロパティが増えるほど、書き忘れて一つだけ書き換え可能なプロパティが混ざる、という事故を防げるのが嬉しいところです。値オブジェクトやDTOのように「全プロパティが不変であるべき」クラスなら、迷わずクラス単位で宣言してしまうのが素直だと思います。
すべてのプロパティに型が必須になる
readonlyクラスでは、プロパティは必ず型宣言を持たなければなりません。型を書き忘れると、実行前にコンパイルエラーになります。
readonly class Broken
{
// 型を書かないとエラーになる
public $value;
}
もともとreadonlyプロパティ自体が「型付きプロパティしか許さない」という制約を持っているので、クラス単位に広げても違和感のあるルールではありません。動的プロパティ(宣言していないプロパティを後から生やす書き方)も一緒に封じられるので、#[AllowDynamicProperties]属性をreadonlyクラスに付けることもできません。
継承するときは何に気をつける?
readonlyクラスを継承すること自体は可能です。ただし、親クラスがreadonlyなら、それを継承する子クラスも明示的にreadonlyを宣言しなければいけません。親だけreadonlyで子は普通のクラス、という中途半端な状態は許されない仕組みです。abstractやfinalとの併用も問題なく、final readonly classのように並べて書けます。順序はどちらが先でも構いません。
まとめ
readonlyクラスは、プロパティ単位のreadonlyを「全部に付け忘れなく適用する」ための糖衣構文という捉え方をしています。DTOや値オブジェクトのように、そもそも全プロパティを不変にしたいクラスでは積極的に使う価値があると思います。逆に一部のプロパティだけ書き換え可能にしたい設計なら、無理にクラス単位にせず、プロパティ単位のreadonlyのままにしておくのが素直です。
よくある質問
Q. readonlyクラスとreadonlyプロパティの違いは何ですか?
A. readonlyプロパティはプロパティ一つずつに付ける修飾子で、readonlyクラスはクラス宣言に付けることで中の全プロパティに自動的にreadonlyを適用する書き方です。効果としては同じ不変性を実現します。
Q. readonlyクラスはインターフェースやトレイトにも使えますか?
A. 使えません。readonlyキーワードが付けられるのはクラス(abstract・finalとの併用含む)のみで、インターフェース、enum、トレイトには付けられません。
Q. 何バージョンから使える機能ですか?
A. readonlyクラスはPHP 8.2で追加されました。プロパティ単位のreadonlyはそれより一つ前のPHP 8.1からです。