12
10

More than 5 years have passed since last update.

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

Posted at

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

  • オブジェクトの作成時は可変で freeze で不変になる
    • 実際には同じプロパティを持つ新しいオブジェクトを作成している
  • 不変オブジェクトはできるだけピュアなオブジェクトにする
    • 余計なプロパティを持たせなくない
    • 例えば $freezed みたいなフラグとか
  • DocComment を書けば入力補完を効かせられる
  • __get__set 以外のマジックメソッドは定義していない
    • ただの手抜き
<?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 で新しいインスタンスを生成するのは実装の都合なのでコンストラクタが呼ばれるのは不自然
12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10