肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

名前空間と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 の実装は簡単で、一言で説明すると、

登録された名前空間とパスを結合して \ を / に変えて .php を付ける

これだけです。

では、

$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 さんにお渡しします!

PHP 5.3の話もちゃんと説明している、パーフェクトPHPもよろしくね!

*1:そういや、Google Groupsってページ機能をサポートしなくなるっぽいけど、このへんのページはどうなるんだろうか・・・

*2:file_exists使うとstatが発生するから、コスト的になーってやつがありますよね、まあ、ErrorException飛ばすようにしろよとかいろいろありますが、個人的にはこれでしっくりくるかんじ