Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

リフレクションと__get/__setメソッドを使ってtypesafeを実現する


リフレクションと__get/__setメソッドをうまく使って実現する方法が紹介されています。


I always disliked the way PHP handles Objects. There is no way to assign a type to properties. Validators have to be glued against the fields externally and you can't just generate a Object-Description (like WSDL) from a object either.

掲載されているコードをざっとまとめてみました。PHP5.2.1で動作確認しています。リフレクションのgetDocCommentメソッドを使って、コード内のコメントに定義された型情報を取得しています。この辺は参考になります :-)
なお、PHPでは多重継承ができないので、そこが難点になるんじゃないでしょうか。

<?php
/**
 * @see http://jan.kneschke.de/2007/2/19/typesafe-objects-in-php
 */
class POPO {
    function hasProperty($k) {
        $r = new ReflectionObject($this);
        return $r->hasProperty($k);
    }

    function getPropertyType($k) {
        $o = new ReflectionObject($this);
        $p = $o->getProperty($k);
        $dc = $p->getDocComment();
        if (!preg_match("#@var\s+([a-z]+)#", $dc, $a)) {
            return false;
        }
        return $a[1];
    }
}

class POPOTypeSafe extends POPO {
    function __get($k) {
        if (!$this->hasProperty($k)) {
            throw new Exception(sprintf("'%s' has no property '%s'", get_class($this), $k));
        }
        if (!isset($this->$k)) {
            return NULL;
        }
        return $this->$k;
    }

    function __set($k, $v) {
        if (!$this->hasProperty($k)) {
            throw new Exception(sprintf("'%s' has no property '%s'", get_class($this), $k));
        }
        if (!($type = $this->getPropertyType($k))) {
            throw new Exception(sprintf("'%s'.'%s' has no type set", get_class($this), $k));
        }
        if (!$this->isValid($k, $v, $type)) {
            throw new Exception(sprintf("'%s'.'%s' = %s is not valid for '%s'", get_class($this), $k, $v, $type));
        }
        $this->$k = $v;
    }

    function isValid($k, $v, $type) {
        if (!isset($v)) return false;
        if (is_null($v)) return false;

        switch ($type) {
        case "int":
        case "integer":
        case "timestamp":
            return (is_numeric($v));
        case "string":
            return true;
        default:
            throw new Exception(sprintf("'%s'.'%s' has invalid type: '%s'", get_class($this), $k, $type));
        }
    }
}

class Employee extends POPOTypeSafe {
    /** @var int*/
    protected $employee_id;

    /**
     * @var string
     */
    protected $name;

    /** @var string */
    protected $surname;

    /** @var timestamp */
    protected $since;
}

$employee = new Employee();
var_dump($employee);

/**
 * nameに対する操作
 */
var_dump($employee->name);
try {
    $employee->name = 'Jan';
} catch (Exception $e) {
    echo $e->getMessage();
}
var_dump($employee->name);

/**
 * employee_idに対する操作
 */
try {
    /**
     * 型が合っていない
     */
    $employee->employee_id = "foobar";
} catch (Exception $e) {
    echo $e->getMessage();
}
try {
    /**
     * 型が合っている
     */
    $employee->employee_id = 1;
} catch (Exception $e) {
    echo $e->getMessage();
}
var_dump($employee->employee_id);

/**
 * 存在しないプロパティ
 */
try {
    $employee->unknown = 1;
} catch (Exception $e) {
    echo $e->getMessage();
}