名前空間とautoload、標準的なClassLoaderの実装 (#phpadvent2010)
ってことで、アドベントカレンダーが回ってきたので書きます。なに書こうか迷いましたが、とりあえず、最近自分でも真面目に使い始めたPHP 5.3向けのClassLoaderと名前空間についての話です。
世の中的にずいぶん「これからはPHP 5.3だよね」的な流れがきているので、名前空間の区切りと、ディレクトリ構成、ファイル名、クラス名など、これから書くならどうするんだろ?ってところについておさらいしておきます。
PHP Standards Working Group
第1回のモダンPHP勉強会で、id:Fivestarが発表したように、Symfonyなどの開発者が集まって、このような内容をPHP界隈でちゃんと取り決めて標準っぽくしようよって話をしている、PHP Standards Working Groupというグループがあります。(最近あんま動きがないな)
このグループで議論がまとまっているものに、PSR-0 Final Proposal があり、この中で、名前空間の使い方・クラス名・ディレクトリの区切りはこうしよう、という意見がまとまってます。*1
例をあげると、以下のような感じ。
- \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
- \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
- \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
- \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php
ちなみにこのページはid:hnwさんが翻訳されています。
SplClassLoader を使ってみよう!
で、本題。標準的な名前空間とクラス名などの命名規則が決まれば、標準的なAutoloadクラスが作れるよねってことで作られたのが、SplClassLoaderです。SPLって名前が付いてるけど、別に組み込みでもないし、PHP的に標準になっているわけでもないので、「提案」レベルのものですが、SymfonyもこれをベースにClassLoaderをもっているし、まあ実質標準的なものであると考えてもらって問題ないです。
ってことで、これを使ってみましょう。
標準的なディレクトリ構成とクラスのファイルを用意
今回は、以下のような感じで作ってみました。
% tree . |-- Hoge | |-- Hogege.php | `-- Hogera | `-- Aho.php |-- SplClassLoader.php |-- entry_point.php `-- libs `-- vendors `-- Fuga `-- Foo.php
entry_point.php が実際にライブラリを使う側のファイル、Hoge と Fuga が、読み込まれる側のライブラリファイルだと想定し、Fuga はどこか変なとこにディレクトリがあるものだとします。まあ、よくある構成だはと思います。Aho.php Hogera.php Foo.php には実際にクラスが定義されていて、とりあえずコンストラクタが呼ばれると自分自身の名前空間とクラス名を出力するようにしてあります。
例えば次のような感じ:
<?php namespace Hoge\Hogera; class Aho { public function __construct() { echo __CLASS__, PHP_EOL; } }
実際に使ってみる
entry_point.php では、次のよう register() メソッドを呼び出し、名前空間とディレクトリのルートを指定したものを登録します。
<?php require 'SplClassLoader.php'; $class_loader_hoge = new SplClassLoader('Hoge', __DIR__); $class_loader_hoge->register(); $class_loader_fuga = new SplClassLoader('Fuga', __DIR__ . '/libs/vendors'); $class_loader_fuga->register(); $a = new Hoge\Hogege(); $b = new Hoge\Hogera\Aho(); $c = new Fuga\Foo();
この出力は、
Hoge\Hogege Hoge\Hogera\Aho Fuga\Foo
となります。
正しくHogeやFugaの名前空間のクラスが読み込まれましたね。
ClassLoaderの動きを追ってみる
ClassLoader の実装は簡単で、一言で説明すると、
これだけです。
では、
$class_loader_hoge = new SplClassLoader('Hoge', __DIR__); $class_loader_hoge->register();
この設定でAutoloadの設定をし、
$b = new Hoge\Hogera\Aho();
こう使ったときに、どう読み込まれるかというと、
<?php // 以下は、SplClassLoaderのロード部分の実装。 public function loadClass($className) { if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) { $fileName = ''; $namespace = ''; if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) { // 名前空間区切りが見つかれば、それを最後の名前空間区切り以下をクラス名として、 // クラス名と名前空間に分解する $namespace = substr($className, 0, $lastNsPos); // => "Hoge\Hogera" $className = substr($className, $lastNsPos + 1); // => "Aho" $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; // 名前空間の区切り文字 "\" を "/" に変換して、パス名にする // => "Hoge/Hogera/" } $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension; // クラス名のアンダースコア "_" を "/" に変換して、拡張子をつける // そしてそれを、パス名に結合し、これをファイル名とする // => "Hoge/Hogera/Aho.php" require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName; // 最後に、設定してあるインクルードパスにファイル名を結合してrequireする // => "/home/sotarok/tmp/splclassloader/Hoge/Hogera/Aho.php" } }
だいたいこんなかんじで、SplClassLoaderのコンストラクタに指定したインクルードパスと、それ以下のパスが名前空間を含んだクラス名とマッピングされ、クラスが読み込まれます。実に単純ですね。
まんまでも使えるけど・・・
実際には、もうちょっとルーズに使いたかったので、registerNamespace()とかのstaticメソッドを定義して使ってます。*2
こうすると、
<?php ClassLoader::registerNamespace('Hoge', '/path/to/Hoge'); ClassLoader::registerNamespace('Fuga', '/path/to/Fuga');
なんてかんじでルーズに使えて素敵です。
というわけで
いよいよ、と思って名前空間を使いはじめると、まだまだノウハウが共有されていなくて(それでも出てきたほうですが)悩むことも多いと思いますが、参考になれば幸いです。
アドベントカレンダーのバトンは slumbers99 さんにお渡しします!