ほげたつブログ

プログラムとアニメーションをかじって生きてる

PHPでメンバ変数の管理をガチガチにしてみた

PHPは型が違っていても問答無用で突っ込めたり、メンバ変数に宣言されていなくても動的に作成できたりして、適当な人がコードを書くと途端にスパゲッティ化してしまう。
他人が書いたコード内のバグを追っている時とか、C++出身の自分としてはこれが非常にもどかしかったりする。
なのでガチガチにしてみた。


https://gist.github.com/HogeTatu/5735002

<?php
abstract class ObjectBase
{
    private $_fieldValueList = array();
    private $_fieldTypeList = array();
    private $_lock = false;

    public final function __construct()
    {
        $privateFieldList = self::_getPrivateFieldList();
        
        foreach ($this as $label => $value) {
            if (isset($privateFieldList[$label])) continue;
            $this->_fieldTypeList[$label] = gettype($value);
            $this->_fieldValueList[$label] = $value;
            unset($this->{$label});
        }

        if (method_exists($this, '_initialize')) {
            call_user_func_array(array($this, '_initialize'), func_get_args());
        }
    }

    private static final function _getPrivateFieldList()
    {
        static $privateFieldList = null;
        if ($privateFieldList !== null) return $privateFieldList;

        $privateFieldList = array();
        $classVars = get_class_vars(get_class());
        foreach ($classVars as $label => $value) {
            $privateFieldList[$label] = $label;
        }
        return $privateFieldList;
    }

    public final function __get($label)
    {
        if (isset($this->_fieldTypeList[$label]) === false) {
            throw new Exception("{$label} is not found in field list.");
        }
        
        return $this->_fieldValueList[$label];
    }

    public final function __set($label, $value)
    {
        if ($this->_lock) {
            throw new Exception("field list is locked.");
        }
        
        if (isset($this->_fieldTypeList[$label]) === false) {
            throw new Exception("{$label} is not found in field list.");
        }

        $oldType = $this->_fieldTypeList[$label];
        $newType = gettype($value);
        if ($oldType !== 'NULL' && $oldType !== $newType) {
            throw new Exception("type of {$label} is {$oldType} rather than {$newType}.");
        }

        $this->_fieldTypeList[$label] = $newType;
        $this->_fieldValueList[$label] = $value;
    }

    public function lock()
    {
        $this->_lock = true;
    }

    public function toArray()
    {
        return $this->_fieldValueList;
    }
}

//=========================================================
class Hoge extends ObjectBase
{
    public $foo = 1;
    public $bar = null;

    protected function _initialize($a, $b)
    {
        echo "a = {$a}\n";
        echo "b = {$b}\n";
    }
}

//=========================================================
$hoge = new Hoge(1, 2);

$hoge->foo = 2;     // OK
//$hoge->foo = 2.5; // NG
echo "{$hoge->foo}\n";

$hoge->bar = 1.5;   // OK
//$hoge->bar = 1;   // NG
echo "{$hoge->bar}\n";

$hoge->lock();
//$hoge->foo = 3;   // NG

var_dump($hoge->toArray());

/*
a = 1
b = 2
2
1.5
array(2) {
  ["foo"]=>
  int(2)
  ["bar"]=>
  float(1.5)
}
*/

メンバ変数に何かをSETしようとした時に、型が違った場合はエラーにする。
NULLで初期化されている時は自由な型で一度だけSETできる。
宣言されていないメンバ変数のGET/SETは認めない。
これでバグを追う時に余計なことを気にしなくて済む。

もちろん動的に何かをやりたくて__getや__setを使いたいクラスでは使えない。
使える場面で使うといった感じで。


【追記】
上記実装ではアクセス制限に関して問題があるため、そこを考慮した実装に修正しなければならない。
また、__getや__setはpublicでの宣言しか許されないため、どこからアクセスされたのかをbacktraceを見て判断しなければならない。
うーむ、ボツだな。