はじめに
ドメイン駆動開発ではimmutableなクラスを使う機会が少なくありませんが(ex.ValueObject)、実装が単純&面倒なあまりせっかちなプログラマの勤労意欲を阻害しがちです。「プログラマは怠惰であれ」の精神に基づき、PHPでインスタントにReadOnlyなクラスを実装する方法を考えてみましょう。
まずは教科書的に書いてみる
メンバをprivateにした上でgetterを作るパターン。
コンストラクタ以外のタイミングではメンバのセットを受け付けません。
作りが明快な一方、プロパティの数だけgetterを作る手間が発生します。
※@property-read
はIDE補完用の記述です。
/**
* @property-read string $name
* @property-read int $age
*/
class Human {
/** @var string */
private $name;
/** @var int */
private $age;
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
public function getName() {
return $this->name;
}
public function getAge() {
return $this->age;
}
}
$human = new Human('ほげ野ぴよ太', '48');
$name = $human->getName(); //OK
$human->name = 'ふが田ぴよ彦'; //NG
面倒なのでgetterを共通化する
マジックメソッドを利用したTraitを使い回せば、getterを個別実装せずともReadOnlyなプロパティを手軽に実装できます。(ポイントはアクセス不能なメソッドのハンドリングです)
trait ReadOnlyTrait {
//__getはアクセス不能なプロパティを参照した際に呼び出されるマジックメソッド
public function __get($name) {
return $this->$name;
}
}
/**
* @property-read string $name
* @property-read int $age
*/
class Human {
use ReadOnlyTrait;
/** @var string */
private $name;
/** @var int */
private $age;
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
}
$human = new Human('ほげ野ぴよ太', '48');
$name = $human->name; //OK
$human->name = 'ふが田ぴよ彦'; //NG
注意点
privateだろうがprotectedだろうが問答無用で外からプロパティを読めてしまうので、隠蔽すべき項目は個別の取り回しが必要です(当たり前ですが)。使用前に必ず適切性を一考しましょう。
2017/4/23 追記
@tadsanの指摘事項を反映しました。private/protectedなプロパティを外部から読み込み可能にするという投稿で、同コンセプトのコードをより精緻に解説してくれています。みんなも参考にしよう!(すぐに使えるライブラリ付き!)