📝
PHPで不変(イミュータブル)オブジェクトの更新について
不変(イミュータブル)オブジェクトは、その名の通り不変なので中身を更新することができません。
なので、更新したい場合は、基本的に更新後の状態の新しいインスタンスを作ることになります。
今回は簡単に更新後のインスタンスを作れるトレイトを考えてみました。
いわゆるコピーメソッドです。
コードの説明
このコードは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);
}
}
copyメソッドの内容
-
copyDefalutValues()
を実行して、コンストラクタに設定すべきインスタンス内のメンバー変数を連想配列で取得 -
array_intersect_key
で変更したい値をマージ - その値を使って新しいインスタンスを作成
やってることはこれだけです。
とてもシンプルですね。
使い方
下記のように使うことができます。
/**
* ↓ 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