PHP で freeze で Immutable になるオブジェクト

# PHP で freeze で Immutable になるオブジェクト

 - オブジェクトの作成時は可変で freeze で不変になる
    - 実際には同じプロパティを持つ新しいオブジェクトを作成している
 - 不変オブジェクトはできるだけピュアなオブジェクトにする
    - 余計なプロパティを持たせなくない
    - 例えば `$freezed` みたいなフラグとか
 - DocComment を書けば入力補完を効かせられる
 - `__get`  と `__set` 以外のマジックメソッドは定義していない
    - ただの手抜き

```php
<?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;
    }
}
```

## ImmutableTrait

 - オブジェクトを Immutable にするためのトレイト
 - `__get` で private と protected なプロパティも取得できる
 - `__set` で private と protected なプロパティの変更時に例外を投げる

## FreezableTrait

 - Immutable なオブジェクトを可変にするためのトレイト
    - ImmutableTrait を use したクラスを継承したクラスで use する
 - freeze で親クラスのインスタンスを作成してプロパティをコピーする
    - このとき親クラスのコンストラクタは呼ばない
    - `Closure::bind` を利用して親クラスの private プロパティもコピーする

## Sample

 - ImmutableTrait を use したイミュータブルなオブジェクト
 - 入力補完のために @property DocComment を書いている
 - private プロパティはコンストラクタでのみ設定可能
 - protected プロパティは SampleMutable からも変更可能
 - public プロパティは作るべきではない
 - private/protected プロパティも `__get` 経由で外部から可視

## SampleMutable

 - FreezableTrait を use したミュータブルなオブジェクト
 - Sample を継承している
    - なので @property DocComment を新たに書かなくてもプロパティの入力補完は効く
 - Sample のコンストラクタは SampleMutable のインスタンス作成時に暗黙に呼ばれる
 - Sample の protected プロパティを `__set` 経由で外部から変更可能
 - freeze で同じプロパティを持つ Sample を返す
    - このときは Sample のコンストラクタを呼ばない
    - freeze で新しいインスタンスを生成するのは実装の都合なのでコンストラクタが呼ばれるのは不自然