📝

PHPで不変(イミュータブル)オブジェクトの更新について

2023/08/28に公開

不変(イミュータブル)オブジェクトは、その名の通り不変なので中身を更新することができません。
なので、更新したい場合は、基本的に更新後の状態の新しいインスタンスを作ることになります。
今回は簡単に更新後のインスタンスを作れるトレイトを考えてみました。
いわゆるコピーメソッドです。

コードの説明

このコードはPHP8.1以上で動きます。

ソースコードはこれが全てです。

trait Copyable
{
    // 誤った推測をするので無視する
    /** @phpstan-ignore-next-line */
    function copy(...$params): static
    {
        $prev = $this->copyDefalutValues();
        $new = array_intersect_key($params, $prev);

        return new static(...[...$prev, ...$new]);
    }

    /**
     * @return array<string, mixed>
     */
    function copyDefalutValues()
    {
        $rf = new \ReflectionClass(self::class);
        $names = array_map(fn ($v) => $v->name, $rf->getConstructor()?->getParameters() ?? []);
        $filter = array_fill_keys($names, 0);
        $defValues = (array)$this;
        return array_intersect_key($defValues, $filter);
    }
}

GitHub

copyメソッドの内容

  1. copyDefalutValues()を実行して、コンストラクタに設定すべきインスタンス内のメンバー変数を連想配列で取得
  2. array_intersect_keyで変更したい値をマージ
  3. その値を使って新しいインスタンスを作成

やってることはこれだけです。
とてもシンプルですね。

使い方

下記のように使うことができます。

/**
 * ↓ PHPStanで型をチェックできるようにしている
 * @method static copy(int $a=self, int $b=self, int $e=self)
 */
final class Hoge
{
    use Copyable;
    function __construct(public readonly int $a, public readonly int $b, public readonly int $e)
    {
    }
}

$hogeA = new Hoge(a: 2, b: 3, e: 888);

// bとeの値だけ変更したコピーを作る
$hogeB = $hogeA->copy(b: 5, e: 222);

//cは存在しないメンバー変数なのでPHPStanでエラーになる
$hogeC = $hogeA->copy(c: 5);

var_dump($hogeA); // a: 2, b: 3, e: 888
var_dump($hogeB); // a: 2, b: 5, e: 222 

あとがき

実は不変(イミュータブル)オブジェクトのクローンを簡単に作るSpatie\Cloneable\Cloneableというパッケージがすでに存在します。
使い方も私の書いたトレイトとほぼ同じです。

じゃあなんでわざわざ作ったのか? と思われるでしょう。
それは、上記のパッケージではnewInstanceWithoutConstructor()を使っているため、クローン作成時にコンストラクタ処理を無視してしまうからです。

不変(イミュータブル)オブジェクトにとって、コンストラクタは重要だと思っています。
なぜなら、不変(イミュータブル)オブジェクトはドメインルールなどの制約をコンストラクタに書くことが多いからです。

そういった経緯でコンストラクタを通るトレイトを作ってみました。

興味のある人は使ってみてください。

Discussion