Skip to content

Instantly share code, notes, and snippets.

@k-holy
Created August 5, 2011 06:42
Show Gist options
  • Save k-holy/1127033 to your computer and use it in GitHub Desktop.
Save k-holy/1127033 to your computer and use it in GitHub Desktop.
PSR-0対応クラスローダ試案
<?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;
/**
* Loader
*
* @author [email protected]
*/
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
namespace Holy\Tests;
use Holy\Loader;
/**
* LoaderTest
*
* @author [email protected]
*/
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());
}
}
@k-holy
Copy link
Author

k-holy commented Aug 20, 2011

テストし直しててテストコード特有の問題が色々と発覚。
オートロードのテストケースの前にLoader::load()メソッドでの手動ロードのテストケースを書いてて、しかも同じクラスをロードしていたため、オートロードのテストになっていなかった。
読込済みのクラスを無効にする関数がないと対処しようがない?素直にテストケース毎に別のクラスを指定した方がいいんだろうか。

で、上の問題に気づいて正しいオートロードのテストを行ったところ、どうやってもLoaderが読込に失敗する。
Loaderのテストケース実行前にStageHand_TestRunnerのテスト準備スクリプト(prepare.php)でLoaderをオートロードに登録していたため、そっちの設定で先にオートロードしようとしてLoaderが例外をスローしてたのが原因だった。
spl_autoload_register()の第3引数$prependをtrueにして、テストケースで登録するLoaderをオートロードスタックの先頭に追加することで回避できたけど、そもそもLoaderでロード失敗時に例外をスローさせるのがおかしいのかも?
(Symfony2、Doctrine2、ZendFrameworkともオートロード処理では例外スローしてないし)
オートロードスタックの動作を調査しないと分からないけど、とりあえずfalse返すようにする…

@k-holy
Copy link
Author

k-holy commented Aug 20, 2011

オートロード用メソッドで例外スローせずにbool値のみ返すようにしたら、spl_autoload_register()の第3引数に関係なく、普通に通った。
普段は自作フレームワークしか使ってないので、今までずっと気付かなかったということか。
そもそもLoaderはsingletonにしてるはずなのに、spl_autoload_register()で設定違いのインスタンスが多重登録できてしまうのが変な気がする。
(spl_autoload_functions()で確認しても、別のオブジェクトIDが振られてる)

@k-holy
Copy link
Author

k-holy commented Aug 23, 2011

呼び出し側のサンプルに、サブ名前空間によって読み込みディレクトリを変更する場合の記述を追加。
合わせて、include_path前提のPEAR形式クラスのトップディレクトリ名とLoader::set()で設定したベンダー名が同じ場合、処理順序の都合でベンダー名で登録されたディレクトリの方を読みにいってしまう不具合が発覚。
名前空間使っていないクラスはとりあえずinclude_pathから先に読みに行く仕様とした方が現実的かも…?
でも明示していない物が優先されるというのは気持ち悪いし、どうしたものか。

@k-holy
Copy link
Author

k-holy commented Sep 16, 2011

上の問題と原因は同じなんだけど、\Foo\Bar\Baz のロード後に Foo_Bar_Bazが呼ばれる場合など、名前空間のクラスとPEAR形式のクラスが併用されている時に "Cannot redeclare class \Foo\Bar\Baz" となる問題が発覚。
もちろん include → include_once にすれば解消できるんだけど、負けな気がするので対応を考え中…。
しかしこれらの問題、PSR-0の仕様に起因するものなので、オリジナル実装のSplClassLoaderでも起きる。
併用禁止とするにしても、spl_autoload_register()でのオートロードの仕組み上、オートローダはファイルの読み込みと結果をbool値で返すしかないので、警告も出せないのが辛い。
どのファイルで"Cannot redeclare class"が起きてるかはPHPのエラーメッセージで確認できるから、それで判断してってことでいいんかな…。

@k-holy
Copy link
Author

k-holy commented Jan 27, 2012

ユニットテスト用にinclude_path前提の別解
https://gist.github.com/1675742

include_path利用かつPEAR形式と名前空間クラスの併設時に起こるエラーの検証
https://gist.github.com/1680669

クラスとしての現実的な対応は、名前空間クラスはベンダー名と基点ディレクトリを直接指定、PEAR形式クラスはinclude_path前提かなあ。

@k-holy
Copy link
Author

k-holy commented Feb 9, 2012

こっちが最新版
https://gist.github.com/1707998

singletonやめて、__invoke()を実装して、spl_autoload_register()をユーザ側に任せただけですが…。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment