somemo's diary

プログラマ、雑記、プログラミング関係はLinkから、数式はこっちでまとめていることが多い

【Zendframework】オートローダー

ユニットテストをしていたときにクラスがうまく読み込めない問題がありました。そのときの解決法のメモです。

オートロード

Zendframeworkのユニットテストでは、phpunit.xmlで指定しているファイルでオートロード設定を行っています。

<phpunit bootstrap="./bootstrap.php">

このbootstrap.phpの下記の記述によってオートロードが実現されています。

require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

しかし、これで読み込まれるクラスは、Zend_*またはZendX_*だけです。これらが読み込まれることでZend_Test系クラスが有効になります。

もう少し詳細に説明すると、Zend_Loader_Autoloaderの_namespaces変数に記載されているものが有効になるようです。

protected $_namespaces = array(
    'Zend_'  => true,
    'ZendX_' => true,
);

問題点

このままでは、Zend系以外のソース(クラス)を用いるテストができません。下記エラーが発生します。

Fatal error: Class 'クラス名' not found in ファイル名 on line 行番号

ただし、例外があります。phpunit.xmlを指定した全てのテストの実施、およびコントローラーのみのテストは実行できます。

それでは、どのテストが実行できないかというと、モデルのテストができません。今までphpunit.xml指定のテストしか実行していなかったので分かりませんでした・・・。

クラス読み込み対象設定

まずは、オートロードの設定を拡張するために、bootstrap.phpを修正します。

$autoloader = Zend_Loader_Autoloader::getInstance();
$autoloader->setFallbackAutoloader(true);

これで、オートロードの設定が有効になりました。対象は、Zendディレクトリと同じlibrary以下のファイルです。

有効なファイル名は、library/My/Dir/File.phpで、クラス名は、My_Dir_Fileです。ディレクトリ階層をアンダースコアで現したクラス名です。

しかし、まだエラーが発生します。モデル以下のクラスには、Applicationというモジュール名?(applicationディレクトリ?)が先頭に含まれているためです。

include_once(Application\Model\Guestbook.php): failed to open stream: No such file or directory

なぜ、httpでは動くのか?

index.phpで生成しているZend_Applicationによって読み込まれるようになっています。

// Zend_Application生成
Zend_Application::__construct()

// オプション設定
$this->setOptions(application.iniの設定);

// Bootstrap設定
$this->setBootstrap($path, $class);
// ※application.iniのアプリケーションの名前空間に関する設定
appnamespace = "Application"
// ※application.iniのBootstrapに関する設定
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"

// bootstrap生成
// class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
// class Zend_Application_Bootstrap_Bootstrap
//     extends Zend_Application_Bootstrap_BootstrapAbstract
$this->_bootstrap = new $class($this);

// リソースのオートロード設定
$this->getResourceLoader();
public function getResourceLoader()
{
    if *1;
    $this->setDefaultResourceType('model');
}

リソースと呼ばれるだけあって、モデルやフォームがあります。他には、プラグインやヘルパーがあり、さらに見たこと無いフィルターやサービスといったディレクトリまで用意されています。

最後のsetDefaultResourceTypeメソッドはよく分かっていないです・・・。

オートロードの場所

Zend_Loader_Autoloaderクラスのコンストラクタにて設定されています。

spl_autoload_register(array(__CLASS__, 'autoload'));

autoloadメソッドのは以下のとおりです。

if ($autoloader instanceof Zend_Loader_Autoloader_Interface) {
    if ($autoloader->autoload($class)) {
        return true;
    }
}

Zend_Application_Module_Autoloaderは、Zend_Loader_Autoloader_Interfaceを実装しているので、ここでオートロードが実行されます。

処理内容は、クラス名から対応するディレクトリを取得し、includeしているだけです。

ユニットテストでの最終設定

クラス読み込み対象設定に加えて以下の設定を追加します。getResourceLoaderメソッドと同様のことをするだけです。

new Zend_Application_Module_Autoloader(array(
    'namespace' => 'Application',
    'basePath'  => realpath(APPLICATION_PATH),
));

これで、クラス個別のユニットテスト個別が行えるようになりました。全体で行えるのは、http同様、Zend_Applicationを生成しているみたいです。echoはさんだら表示されました。

これだけ調べといてあれですが、5.3系の修正をとりこんだフレームワークが主流になったらオートロードの仕組みも変わると思うと切ないです。

参考

*1:null === $this->_resourceLoader) && (false !== ($namespace = $this->getAppNamespace())) ) { $r = new ReflectionClass($this); $path = $r->getFileName(); $this->setResourceLoader(new Zend_Application_Module_Autoloader(array( 'namespace' => $namespace, 'basePath' => dirname($path), ))); } return $this->_resourceLoader; }

getResourceLoaderメソッドで呼ばれているsetResourceLoaderの引数Zend_Application_Module_AutoloaderによってApplication以下も読み込まれるようになります。

以下、詳細です。

Zend_Application_Module_AutoloaderはZend_Loader_Autoloader_Resourceを継承しています。

生成時に、Zend_Loader_Autoloader_Resourceのコンストラクタを実行します。

コンストラクタ内のオプション設定メソッドで、Zend_Application_Module_Autoloader生成時の配列引数から、設定ファイルのApplication名前空間とapplicationディレクトリを紐付けています。

その後、Zend_Application_Module_Autoloaderコンストラクタ内のinitDefaultResourceTypesメソッドにより、おそらくmodule内にあるはずのディレクトリの名前空間とディレクトリを紐付けています。以下、initDefaultResourceTypesメソッドです。

/**
 * Initialize default resource types for module resource classes
 *
 * @return void
 */
public function initDefaultResourceTypes()
{
    $basePath = $this->getBasePath();
    $this->addResourceTypes(array(
        'dbtable' => array(
            'namespace' => 'Model_DbTable',
            'path'      => 'models/DbTable',
        ),
        'mappers' => array(
            'namespace' => 'Model_Mapper',
            'path'      => 'models/mappers',
        ),
        'form'    => array(
            'namespace' => 'Form',
            'path'      => 'forms',
        ),
        'model'   => array(
            'namespace' => 'Model',
            'path'      => 'models',
        ),
        'plugin'  => array(
            'namespace' => 'Plugin',
            'path'      => 'plugins',
        ),
        'service' => array(
            'namespace' => 'Service',
            'path'      => 'services',
        ),
        'viewhelper' => array(
            'namespace' => 'View_Helper',
            'path'      => 'views/helpers',
        ),
        'viewfilter' => array(
            'namespace' => 'View_Filter',
            'path'      => 'views/filters',
        ),