Quantcast
Channel: 親方、空から覚え書きが!
Viewing all articles
Browse latest Browse all 15

静的初期化子をクラスローダでエミュレートしてシュレディンガーのひこにゃんを解決する方法

$
0
0

hikojiru.pngPHP ではクラスの静的メンバの初期化に定数しか利用できません。例えばオブジェクトをセットしたい場合、どこかのタイミングで初期化してやる必要があります。このタイミングが問題になる例をあげてみます。Hoge は自分のコンストラクタが呼び出された回数を Hoge::$profile->construnction から取得できるクラスです。
class Hoge
{
    public static $profile;

    public function __construct()
    {
        if (self::$profile === null)
        {
            self::$profile = (object)array('construction' => 0);
        }
        
        self::$profile->construction++;
    }
}

// まだ参照しちゃ、らめ!
// echo Hoge::$profile->construction; // Notice: Trying to get property of non-object

// これはよい。
new Hoge();
echo Hoge::$profile->construction; // 1

一度もインスタンスを作成していない場合、Hoge::$profile は NULL のままです。クラス定義の後に続けて Hoge::$profile = (object)array('construction' => 0); としてやれば回避できますが、フローがクラス定義の外へ追い出されてしまいます。そこで、Java の静的初期化子や C# の静的コンストラクタを模倣する手段を考えました。PHP は存在しないクラスが要求された場合にオートローディングが働くよう仕組まれています。これを利用して、オートローダが呼ばれたタイミングで初期化用の静的メソッドをコールすればいいのではと思いました。

実装例を上げます。静的初期化子をサポートするクラスのマーカー的な interface と、それに対応したクラスローダを定義します。

interface IStaticInitializable
{
    static function initializeMember();
}

class ClassLoader
{
    protected $baseClassPath;
    
    public function __construct($baseClassPath)
    {
        $this->baseClassPath = $baseClassPath . DIRECTORY_SEPARATOR;
    }
    
    public function loadClass($name)
    {
        $patterns = array(DIRECTORY_SEPARATOR, ’.’, '_', '::');
        $replacements = array('', '', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
        $filePath = $this->baseClassPath . str_replace($patterns, $replacements, $name) . '.php';
        if (file_exists($filePath))
        {
            require_once $filePath;
        }
        
        if (class_exists($name, false))
        {
            $class = new ReflectionClass($name);
            if ($class->implementsInterface('IStaticInitializable'))
            {
                eval("$name::initializeMember();");
            }
        }
    }
}

ここから IStaticInitializable の実装例を上げます。静的変数(クラスのそれではなく、スコープ修飾子。紛らわしい…)で二度初期化しないよう定義しています。ファイル名はみんな大好き Hikonyann.php です。

class Hikonyann implements IStaticInitializable
{
    public static $stacks;
    public static $eatings;
    public static $initializedBy;
    public static $initializedAt;
    
    public static function initializeMember()
    {
        static $initialized = false;
        
        if (!$initialized)
        {
            self::$stacks = mt_rand(0, 100);
            self::$eatings = mt_rand(0, 100);
            ob_start(); debug_print_backtrace(); $backtrace = ob_get_clean();
            self::$initializedBy = $backtrace;
            self::$initializedAt = time();
            
            $initialized = true;
        }
    }
}

クラスの利用側。ファイルはクラスと同じディレクトリにあると仮定します。

<?php
$loader = new ClassLoader(dirname(__FILE__));
spl_autoload_register(array($loader, 'loadClass'));

printf(
    'ひこにゃんは %2$d 回通路に挟まりました。%1$s'
    . 'ひこにゃんは %3$d 個のだんごを食べました。%1$s'
    . '状態が観測されたのは %4$s です。%1$s'
    . '観測者は次の通りです。%1$s%5$s',
    PHP_EOL,
    Hikonyann::$stacks,
    Hikonyann::$eatings,
    date(DATE_RFC3339, Hikonyann::$initializedAt),
    Hikonyann::$initializedBy
    );

そして出力例。

ひこにゃんは 85 回通路に挟まりました。
ひこにゃんは 27 個のだんごを食べました。
状態が観測されたのは 2008-05-01T12:32:39+09:00 です。
観測者は次の通りです。
#0  Hikonyann::initializeMember() called at [#:\###########\sample.php(32) : eval()'d code:1]
#1  eval() called at [#:\###########\sample.php:32]
#2  ClassLoader->loadClass(Hikonyann)
#3  spl_autoload_call(Hikonyann) called at [#:\###########\sample.php:47]

これで初期化を意識せず済むようになりました。


Viewing all articles
Browse latest Browse all 15

Trending Articles