Created
August 5, 2011 06:42
-
-
Save k-holy/1127033 to your computer and use it in GitHub Desktop.
PSR-0対応クラスローダ試案
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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()で指定したベンダー名が同じ場合はベンダーの方がよばれて二重定義エラーになってしまう… |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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()); | |
} | |
} |
上の問題と原因は同じなんだけど、\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のエラーメッセージで確認できるから、それで判断してってことでいいんかな…。
ユニットテスト用にinclude_path前提の別解
https://gist.github.com/1675742
include_path利用かつPEAR形式と名前空間クラスの併設時に起こるエラーの検証
https://gist.github.com/1680669
クラスとしての現実的な対応は、名前空間クラスはベンダー名と基点ディレクトリを直接指定、PEAR形式クラスはinclude_path前提かなあ。
こっちが最新版
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
呼び出し側のサンプルに、サブ名前空間によって読み込みディレクトリを変更する場合の記述を追加。
合わせて、include_path前提のPEAR形式クラスのトップディレクトリ名とLoader::set()で設定したベンダー名が同じ場合、処理順序の都合でベンダー名で登録されたディレクトリの方を読みにいってしまう不具合が発覚。
名前空間使っていないクラスはとりあえずinclude_pathから先に読みに行く仕様とした方が現実的かも…?
でも明示していない物が優先されるというのは気持ち悪いし、どうしたものか。