<?php
trait ImmutableTrait
{
public function __get($name)
{
if (!property_exists($this, $name))
{
$class = get_class($this);
throw new \LogicException("Undefined property \"$class::$name\".");
}
return $this->$name;
}
public function __set($name, $val)
{
$class = get_class($this);
throw new \LogicException("Cannot modify property \"$class::$name\".");
}
}
trait FreezableTrait
{
public function __set($name, $val)
{
if (!property_exists($this, $name))
{
$class = get_class($this);
throw new \LogicException("Undefined property \"$class::$name\".");
}
$this->$name = $val;
}
public function freeze()
{
$obj = (new ReflectionObject($this))->getParentClass()->newInstanceWithoutConstructor();
call_user_func(\Closure::bind(function ($obj) {
$vars = get_object_vars($this);
foreach ($vars as $name => $val)
{
$obj->$name = $val;
}
}, $this, $obj), $obj);
return $obj;
}
}
/**
* @property $val int
* @property $priv int
*/
class Sample
{
use ImmutableTrait;
protected $val;
private $priv;
public function __construct($val, $priv)
{
$this->val = $val;
$this->priv = $priv;
}
}
class SampleMutable extends Sample
{
use FreezableTrait;
}
class FreezableTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
function freeze_()
{
$obj = new SampleMutable(999, 888);
// Sample のコンストラクタで値がセットされている
assertEquals(999, $obj->val);
assertEquals(888, $obj->priv);
// SampleMutable なので変更できる
$obj->val = 111;
assertEquals(111, $obj->val);
// freeze で immutable にする
$obj = $obj->freeze();
assertEquals(111, $obj->val);
assertEquals(888, $obj->priv);
// 変更しようとすると例外
$this->setExpectedException(\LogicException::class, 'Cannot modify');
$obj->val = 789;
}
/**
* @test
*/
function priv_()
{
$obj = new SampleMutable(999, 888);
// Sample のプライベートプロパティは SampleMutable でも可視だけど変更不可
assertEquals(888, $obj->priv);
$this->setExpectedException(\LogicException::class, 'Undefined property');
$obj->priv = 777;
}
/**
* @test
*/
function undef_()
{
$obj = new Sample(999, 888);
$this->setExpectedException(\LogicException::class, 'Undefined property');
$obj->undef;
}
}