明滅するプログラマの思索

WEBエンジニアとして勤務している一介の男が、日々気づいたことをまとめるブログです

クラスにメソッドを動的に追加する trait

実装済みのクラスに、あとからメソッドを動的に追加することができるようにします。

実装環境

ソフトウェア バージョン
PHP 5.6.30

※ ただし trait 自体は PHP5.4 以降で利用可能

コード

まずは処理の核となる trait を実装します。

<?php
/**
 * クラスにメソッドを動的に追加できるようにする trait
 *
 * @property \Closure[]
 *
 */
trait DynamicMethod
{
    static private $methods = [];

    static public function addMethod($name, $func)
    {
        self::$methods[$name] = $func;
        return true;
    }

    /**
     * @param string $name
     * @param array $arg
     * @return mixed
     * @throws Exception
     */
    public function __call($name, $arg)
    {
        if (isset(self::$methods[$name])) {
            $func = self::$methods[$name];
            return call_user_func_array($func->bindTo($this, get_class($this)), $arg);
        }
        $parent_callable = false;
        foreach (class_parents($this) as $parent) {
            if (method_exists($parent, '__call')) {
                $parent_callable = true;
                break;
            }
        }
        if ($parent_callable) {
            return parent::__call($name, $arg);
        }
        throw new Exception(sprintf("Call to undefined method %s:%s()", get_class($this), $name));
    }
}

上記の DynamicMethod trait は static な private メンバ変数 $methods を持ちます。 addMethod() メソッドを実行することで $methods に実行するクロージャを格納できます。
マジックメソッド __call() がコールされると、$methods に格納されているメソッドを検索し、見つかればそれを実行します。
このとき bindTo() を使って、各メソッド内で $this を使えるようにしています。
見つからなかった場合、親クラスに __call() があるかを確認します。あれば処理を引き渡し、なければ Exception を throw します。

上記 trait を利用するクラスでは、trait を use するだけです。

<?php
class Hoge
{
    use DynamicMethod;
}

メソッドを追加するときは、addMethod() を呼び出します。

<?php
Hoge::addMethod('echoClassName', function() {
    echo get_class($this);
});

$hoge= new Hoge();
$hoge->echoClassName();  // コール可能。'Hoge' が出力される

$hoge->fuga(); // 定義されていないので Exception となる