PHP
PHP小ネタ

PHPで不変(Immutable)オブジェクト

More than 4 years have passed since last update.

 コントローラからビューにオブジェクトを渡す場合、場合によってはビュー側で操作されたくないケースもあるかと思います。

 そのような際に役立つかもしれないのが 不変(Immutable)オブジェクト。普通はImmutableな別クラスとして定義するのだと思いますが、それだと実装が複雑になってしまうので、変更操作(プロパティへのsetter、または変更メソッド)に対して例外を投げるクラスを作成してみました。

<?php
class Immutable{
    private $obj;
    private $setters_pattern;

    public function __construct( $obj, $setters_pattern = '/set([0-9a-zA-Z])*/' ){
        $this->obj = $obj;
        $this->setters_pattern = $setters_pattern;
    }

    private function checkSetters( $name ){
        if ( is_string($this->setters_pattern) ){
            return preg_match( $this->setters_pattern, $name );
        }
        else if ( is_array($this->setters_pattern) ){
            foreach( $this->setters_pattern as $pattern ){
                if ( preg_match( $pattern, $name ) )    return TRUE;
            }
            return FALSE;
        }
        return FALSE;
    }

    public function __get( $key ){
        // 内包オブジェクトに対応するプロパティがある場合は取得
        if ( property_exists( $this->obj, $key ) ){
            return $this->obj->$key;
        }
        // えっ!?
        throw new LogicException('えっ!?');
    }

    public function __set( $key, $value ){
        // $foo->barでプロパティを設定した場合は例外をスロー
        throw new Exception( "Immutableオブジェクトのため変更できません:$key" );
    }

    public function __call($name, $arguments){
        // setters_patternにマッチしたメソッドは例外をスロー
        if ( $this->checkSetters( $name ) ){
            throw new Exception( "Immutableオブジェクトのため変更できません:$name" );
        }
        // 内包オブジェクトに対応するメソッドがある場合は実行
        else if ( method_exists( $this->obj, $name ) ){
            return call_user_func_array( array($this->obj,$name), $arguments );
        }
        // えっ!?
        throw new LogicException('えっ!?'.$name);
    }
}

// テストコード
class Hoge
{
    public $foo = 'apple';
    private $bar = 'banana';

    /* getterメソッド */
    public function getBar() { return $this->bar; }

    /* setterメソッド */
    public function setBar($new_val) { $this->bar = $new_val; }

    /* setterではないがbarを変更するメソッド */
    public function changeBar() { $this->bar = 'hoge'; }
}

// 内包されるオブジェクト
$hoge = new Hoge();
// Immutableオブジェクト
$setter_patterns = array(
            '/set([0-9a-zA-Z])*/',
            '/change([0-9a-zA-Z])*/',
        );
$immutable_hoge = new Immutable( $hoge, $setter_patterns );

// プロパティ参照(getter)
print $immutable_hoge->foo . PHP_EOL;   // apple

// メソッドで参照
print $immutable_hoge->getBar() . PHP_EOL;  // banana

// 変更メソッドを呼び出す(setter)
try{
    $immutable_hoge->setBar( 'melon' );    // 例外が発生
}
catch( Exception $e ){
    print $e->getMessage() . PHP_EOL;
}

// 変更メソッドを呼び出す
try{
    $immutable_hoge->changeBar();    // 例外が発生
}
catch( Exception $e ){
    print $e->getMessage() . PHP_EOL;
}

// プロパティアクセスで設定もダメ
try{
    $immutable_hoge->foo = 'pear';    // 例外が発生
}
catch( Exception $e ){
    print $e->getMessage() . PHP_EOL;
}

// 元オブジェクト経由では普通に変更できる
$hoge->setBar( 'orange' );
print $immutable_hoge->getBar() . PHP_EOL;  // orange

実行結果
apple
banana
Immutableオブジェクトのため変更できません:setBar
Immutableオブジェクトのため変更できません:changeBar
Immutableオブジェクトのため変更できません:foo
orange

使い方は、保護したいオブジェクトをコンストラクタに渡し、オプションで変更メソッドのパターンを文字列または配列で渡します。

// 保護したいオブジェクトを作成
$hoge = new Hoge();
// Immutableオブジェクトを作成
$immutable_hoge = new Immutable( $hoge, $setter_patterns );

パターンを省略した場合は"set"で始まるメソッドをすべて変更メソッドとみなします。

$setter_patternsにマッチするメソッドが呼び出されるか、プロパティへのsetが行われた場合、例外をスローします。