ryoakg
8/5/2011 - 6:42 AM

PSR-0対応クラスローダ試案

PSR-0対応クラスローダ試案

<?php
namespace Holy;

/**
 * Loader
 *
 * @author     k.holy74@gmail.com
 */
class Loader
{

    protected static $instance = null;
    protected $autoloadEnabled = false;
    protected $vendorInfo = array();
    protected $fileExtension = '.php';
    protected $namespaceSeparator = '\\';

    protected function __construct()
    {
        $this->vendorInfo = array();
        $this->autoloadEnabled = false;
    }

    /**
     * シングルトンインスタンスを返します。
     * @return object Holy\Loader
     */
    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * シングルトンインスタンスをクリアします。
     */
    public static function resetInstance()
    {
        self::$instance = null;
    }

    /**
     * ベンダー名および設置パス、拡張子を設定します。
     * @param string ベンダー名(namespace)
     * @param string 設置パス
     * @param string 拡張子
     */
    public function set($name, $includeDir, $fileExtension=null)
    {
        $includeDir = rtrim($includeDir, '\\/');
        if ('\\' === DIRECTORY_SEPARATOR) {
            $includeDir = str_replace('/', '\\', $includeDir);
        }
        $this->vendorInfo[$name] = array($includeDir, $fileExtension);
        return $this;
    }

    /**
     * オートローダーによるクラスローディングを有効にします。
     *
     * @param bool SPLオートローダースタックの先頭に追加するかどうか
     */
    public function enableAutoload($prepend = false)
    {
        if ($this->autoloadEnabled) {
            spl_autoload_unregister(array($this, 'load'));
        }
        spl_autoload_register(array($this, 'load'), true, $prepend);
        $this->autoloadEnabled = true;
        return $this;
    }

    /**
     * オートローダーによるクラスローディングを無効にします。
     */
    public function disableAutoload()
    {
        if ($this->autoloadEnabled) {
            spl_autoload_unregister(array($this, 'load'));
        }
        return $this;
    }

    /**
     * 指定されたクラスのファイルを読み込みます。
     * @param string クラス名
     */
    public function load($className)
    {
        $filePath = $this->findFile($className);
        if (false === $filePath) {
            return false;
        }
        include $filePath;
        return true;
    }

    protected function findFile($className)
    {
        $className = ltrim($className, $this->namespaceSeparator); // 先頭の名前空間セパレータは無条件で除去
        $useNamespace = false;
        if (false !== ($pos = strrpos($className, $this->namespaceSeparator))) { // 名前空間セパレータが使われてるかどうか
            $useNamespace = true;
            $namespace = substr($className, 0, $pos);
            $className = substr($className, $pos + 1);
            $fileName = str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) // 名前空間に含まれるアンダースコアを DIRECTORY_SEPARATOR に
                . DIRECTORY_SEPARATOR . str_replace('_', DIRECTORY_SEPARATOR, $className); // クラス名に含まれるアンダースコアを DIRECTORY_SEPARATOR に
        } else {
            $fileName = str_replace('_', DIRECTORY_SEPARATOR, $className); // クラス名に含まれるアンダースコアを DIRECTORY_SEPARATOR に
        }
        $requirePath = null;
        foreach ($this->vendorInfo as $vendorName => $info) { // ベンダー毎に指定されたディレクトリと拡張子でファイルを検索
            $includeDir = $info[0];
            $fileExtension = (isset($info[1])) ? $info[1] : $this->fileExtension;
            if (0 === strpos(($useNamespace) ? $namespace : $className, $vendorName)) {
                $path = $includeDir . DIRECTORY_SEPARATOR . $fileName . $fileExtension;
                if (file_exists($path)) {
                    $requirePath = $path;
                    break;
                }
            }
        }
        if (is_null($requirePath)) {
            $requirePath = stream_resolve_include_path($fileName . $this->fileExtension); // include_path からファイルを検索
        }
        return $requirePath;
    }

}
<?php
// PSR-0 の対応実験 こんなふうに書きたい
namespace Holy\Example;

$lib_path = realpath(__DIR__ . '/../lib');
$test_path = realpath(__DIR__ . '/../tests');

set_include_path($lib_path . PATH_SEPARATOR . get_include_path());

require_once realpath($lib_path . '/vendor/Holy/Loader.php');

\Holy\Loader::getInstance()
->set('Holy'               , realpath($lib_path . '/vendor'))
->set('Smorty'             , realpath($lib_path . '/vendor/Smorty/libs'), '.class.php')
->enableAutoload();

// 現在の名前空間以下のクラス
$foo = new Foo();         // /path/to/lib/vendor/Holy/Example/Foo.php
$bar = new Foo\Bar();     // /path/to/lib/vendor/Holy/Example/Foo/Bar.php
$baz = new Foo\Bar\Baz(); // /path/to/lib/vendor/Holy/Example/Foo/Bar/Baz.php
$sub_package_klass_file = new sub_package\Klass\File(); // /path/to/lib/vendor/Holy/sub_package/Klass/File.php

// singletonなので設定し直しても前の設定は残ります
\Holy\Loader::getInstance()
->set('AutoloadSample\Foo' , realpath($lib_path . '/vendor'))
->set('AutoloadSample\Test', $test_path)
->enableAutoload();

// すまてぃ形式のクラスファイルもOK
$smorty = new \Smorty(); // /path/to/lib/vendor/Smorty/libs/Smorty.class.php

// PEAR形式 (include_path以下) Holy/Example.php
$example = new \Holy_Example();

// 別名前空間のクラス
$sample = new \AutoloadSample\Foo\Bar(); // /path/to/lib/vendor/AutoloadSample/Foo/Bar.php

// サブ名前空間がTestの場合のみ別ディレクトリから読み込む
$test = new \AutoloadSample\Test\FooTest(); // /path/to/tests/AutoloadSample/Test/FooTest.php

//$example = new \Holy_Example_Foo(); // include_path内のトップディレクトリ名とLoader::set()で指定したベンダー名が同じ場合はベンダーの方がよばれて二重定義エラーになってしまう…
<?php
namespace Holy\Tests;

use Holy\Loader;

/**
 * LoaderTest
 *
 * @author     k.holy74@gmail.com
 */
class LoaderTest extends \PHPUnit_Framework_TestCase
{

    protected $defaultLoaders = array();
    protected $defaultIncludePath = null;

    public function setUp()
    {
        $this->defaultLoaders = spl_autoload_functions();
        if (!is_array($this->defaultLoaders)) {
            $this->defaultLoaders = array();
        }
        $this->defaultIncludePath = get_include_path();
        Loader::resetInstance();
    }

    public function tearDown()
    {
        $loaders = spl_autoload_functions();
        if (is_array($loaders)) {
            foreach ($loaders as $loader) {
                spl_autoload_unregister($loader);
            }
        }
        if (is_array($this->defaultLoaders)) {
            foreach ($this->defaultLoaders as $loader) {
                spl_autoload_register($loader);
            }
        }
        set_include_path($this->defaultIncludePath);
        Loader::resetInstance();
    }

    public function testSingleton()
    {
        $this->assertInstanceOf('\Holy\Loader', Loader::getInstance());
        $this->assertSame(
            Loader::getInstance(),
            Loader::getInstance());
    }

    public function testLoadingNamespacedClass()
    {
        Loader::getInstance()
            ->set('LoadSample', realpath(__DIR__ . '/LoaderTest/lib/vendor'))
            ->load('\LoadSample\Foo');
        $this->assertInstanceOf('\LoadSample\Foo', new \LoadSample\Foo());

        Loader::getInstance()
            ->load('\LoadSample\Foo\Bar');
        $this->assertInstanceOf('\LoadSample\Foo\Bar', new \LoadSample\Foo\Bar());

        Loader::getInstance()
            ->load('\LoadSample\Foo\Bar\Baz');
        $this->assertInstanceOf('\LoadSample\Foo\Bar\Baz', new \LoadSample\Foo\Bar\Baz());
    }

    public function testAutoloadingNamespacedClass()
    {
        Loader::getInstance()
            ->set('AutoloadSample', realpath(__DIR__ . '/LoaderTest/lib/vendor'))
            ->enableAutoload(true);
        $this->assertInstanceOf('\AutoloadSample\Foo', new \AutoloadSample\Foo());
        $this->assertInstanceOf('\AutoloadSample\Foo\Bar', new \AutoloadSample\Foo\Bar());
        $this->assertInstanceOf('\AutoloadSample\Foo\Bar\Baz', new \AutoloadSample\Foo\Bar\Baz());
    }

    public function testAutoloadingLegacyClassWithDirectoryAndExtension()
    {
        Loader::getInstance()
            ->set('Smorty', realpath(__DIR__ . '/LoaderTest/lib/vendor/Smorty/libs'), '.class.php')
            ->enableAutoload();
        $this->assertInstanceOf('\Smorty', new \Smorty());
    }

    public function testAutoloadingLegacyClassInIncludePath()
    {
        set_include_path(realpath(__DIR__ . '/LoaderTest/include_path'));
        Loader::getInstance()
            ->enableAutoload();
        $this->assertInstanceOf('\PearStyleClass_Example', new \PearStyleClass_Example());
    }

}