あっぽログ
← 記事一覧に戻る

PHPのReadonly プロパティを使いこなす:イミュータブルなデータ設計を実現する

readonlyプロパティとは

PHP8.1から導入された readonly キーワードを使うと、一度だけ値を設定できるプロパティを定義できます。設定後に値を変更しようとするとエラーになるため、イミュータブル(不変)なオブジェクト設計を簡潔に実現できます。

従来のPHPでイミュータブルなプロパティを実現するには、private アクセス修飾子とゲッターメソッドを組み合わせる必要がありました。

// 従来の方法(冗長)
class User
{
    private string $name;
    private string $email;

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

    public function getName(): string
    {
        return $this->name;
    }

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

readonly を使うと、ゲッターなしで同じ意図をシンプルに表現できます。

readonlyプロパティの基本構文

class User
{
    public readonly string $name;
    public readonly string $email;

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

$user = new User('Alice', 'alice@example.com');

echo $user->name;  // Alice
echo $user->email; // alice@example.com

// 再代入しようとするとエラー
$user->name = 'Bob'; // Error: Cannot modify readonly property

public readonly と宣言することで、外部から読み取りは可能・書き込みは不可という状態を作れます。

注意点:型宣言が必須

readonly プロパティには必ず型宣言が必要です。型なしで宣言するとエラーになります。

// NG: 型宣言なしはエラー
public readonly $name;

// OK: 型宣言あり
public readonly string $name;

コンストラクタプロモーションとの組み合わせ

PHP8.0で追加されたコンストラクタプロモーションと組み合わせると、さらに記述量を減らせます。

class Point
{
    public function __construct(
        public readonly float $x,
        public readonly float $y,
        public readonly float $z = 0.0,
    ) {}
}

$point = new Point(1.5, 2.0);

echo $point->x; // 1.5
echo $point->y; // 2.0
echo $point->z; // 0.0

コンストラクタの引数に public readonly を付けるだけで、プロパティの宣言・代入・公開をすべて1行で完結できます。Value Object(値オブジェクト)パターンを実装する際に非常に便利です。

wither パターンで「変更した新しいオブジェクト」を作る

readonly プロパティは再代入できませんが、値を変えた新しいオブジェクトを返すメソッド(witherパターン)と組み合わせることで柔軟に扱えます。

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

    public function withAmount(int $amount): static
    {
        return new static($amount, $this->currency);
    }

    public function withCurrency(string $currency): static
    {
        return new static($this->amount, $currency);
    }

    public function add(Money $other): static
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('通貨が異なります');
        }

        return new static($this->amount + $other->amount, $this->currency);
    }
}

$price   = new Money(1000, 'JPY');
$tax     = new Money(100,  'JPY');
$total   = $price->add($tax);

echo $total->amount;   // 1100
echo $total->currency; // JPY

// 元のオブジェクトは変わらない
echo $price->amount; // 1000

このパターンにより、元のオブジェクトを壊さずに変更後の状態を表現できます。

PHP8.2:readonly class

PHP8.2では、クラス全体に readonly を付けられるようになりました。すべてのプロパティを自動的に readonly にしたい場合に便利です。

readonly class Coordinate
{
    public function __construct(
        public float $latitude,
        public float $longitude,
    ) {}
}

$coord = new Coordinate(35.6895, 139.6917);

echo $coord->latitude;  // 35.6895
echo $coord->longitude; // 139.6917

// すべてのプロパティがreadonlyになる
$coord->latitude = 0.0; // Error: Cannot modify readonly property

readonly class を使うことで、クラスがイミュータブルであることをコードのシグネチャとして明示できます。DTO(Data Transfer Object)やValue Objectの設計に最適です。

readonlyを使うべき場面

場面理由
Value Object(金額・座標など)値の不変性を保証したい
DTO(APIレスポンスのマッピング等)転送中に値が変わるべきでない
イベントオブジェクト発生したイベントの内容は変えない
設定値のホルダー設定は起動後に変わるべきでない

まとめ

  • readonly プロパティはPHP8.1以降で利用可能で、一度だけ代入できる不変プロパティを定義できる
  • 型宣言が必須で、コンストラクタプロモーションと組み合わせると記述が非常に簡潔になる
  • witherパターンと組み合わせることで、イミュータブルなままオブジェクトの値を変えた新しいインスタンスを返せる
  • PHP8.2の readonly class を使えば、クラス全体のプロパティをまとめてreadonlyにできる

readonly を活用することで、意図せぬ値の書き換えを防ぎ、バグの少ない堅牢な設計を実現できます。Value ObjectやDTOを書く際にはぜひ積極的に取り入れてみてください。

← 記事一覧に戻る