Dispatcher_AliasDirを書いた

一つのモジュールを複製して使いたいというニーズがあったので、エイリアスディレクトリにディスパッチできる機能を作ってみた。モジュールをコピペしたりsymlinkしたりするのではなく、たとえばビュースクリプトの一部だけ上書きするような使い方を可能にする。
たとえば、ブログモジュールを書いたとして、一つのサイトで複数のブログを持つ場合、各ブログ毎のルーティングを作成して、ルートからパラメーターを受け取り、コンフィグを読み分ける方法もある。また、ひとつのサイトで複数のユーザーがいて、それぞれのユーザーが複数のブログを持ちうる場合も内部で区分けする機能を追加すれば実装できる。
しかし、どちらの方法についても、単純な一つのブログモジュールではなく、複雑な条件を加味したコンプレックスなシステムを書くことになる。もう少し進めて、ユーザー毎、ブログ毎にビュースクリプトを自由に変更できるようなシステムにしたいとすると、作成されるビュースクリプトをコンフィグだけで切り分けていく等を考える必要があり複雑度が増す。インスタンス間のリンクを貼る場合にもそれぞれのルート名および個別インスタンスを特定するためのパラメーターを含めなければならない。無論、それでもかまわないが、エイリアスディレクトリを使えるようにすると、モジュールに対する多重設定をクリーンに区分けできるようになる。

モジュールのエイリアスを作ってみる。

そこで、ZFのDispatcher_Standardを参考に、モジュールのエイリアスを作成できるようにしてみた。
動作原理としては、エイリアス用のディレクトリを作り、そこをモジュール用のディレクトリとして利用するが、ディスパッチャからコールするコントローラーは実在するモジュールを呼ぶ。
イメージとしては、「実在モジュールがクラスでエイリアスインスタンス」、「実在モジュールがスーパークラスエイリアスが子クラス」そんな感じ。その識別をディレクトリ単位でできるようにした。そんな感じ。
いろいろまとめて3月中にはリリースしたいんだが、間に合うだろうか。
http://d.hatena.ne.jp/noopable/20090221/1235176875
一応、ここで作成した_prepareを改造した部分が主になる。
動作確認済みだけれど、まさに動いただけっていう感じで、まだまだリファクタリングが必要。
実コントローラーをラップしてディレクトリを必要とするヘルパー等に伝播する方法を考えないといけない。

使用方法と具体部分

使うときは、フロントコントローラーの調整をしていなければ、ディスパッチャに直接登録する

<?php
$dispatcher = $front->getDispatcher();
$dispatcher->addAliasControllerDirectory('../../sections/test/controllers', 'testmodule');

こんな感じで。
testディレクトリにはalias.iniを配置しておく。

[module]
realModule = admin
;alias設定されたコントローラー。アクションはエイリアスできない

[controllers : module]
;このモジュールのコントローラーへの共通設定
common.var1 = value1

[index : controllers]
realController = index
defaultaction = index
[fuga : controllers]
realController = atgyosei_entry
defaultaction = update

AliasDirの_prepare

<?php
    protected function _prepare(Zend_Controller_Request_Abstract $request, $dryrun = false)
    {
        $module     = $request->getModuleName();
        $aliasDirs  = $this->getAliasControllerDirectory();
        $realDirs   = $this->getRealControllerDirectory();
        /*
         * エイリアスかどうかを判定。エイリアスなら設定を読み込んで実コントローラーを特定
         */
        if ($this->isValidAliasModule($module)) {
            $settingFile    = $aliasDirs[$module] . DIRECTORY_SEPARATOR . $this->_aliasSettingsFileName;
            $aliasConf      = new Zend_Config_Ini($settingFile, $request->getControllerName());
            $realModule     = $aliasConf->get($this->_realModuleKey, false);
            $realController = $aliasConf->get($this->_realControllerKey, $request->getControllerName());
            if ($realModule && $realController) {
                $isAlias = true;
            } else {
                var_export($aliasConf->toArray());
                require_once 'Zend/Controller/Exception.php';
                throw new Zend_Controller_Exception('miss-configuration in module aliasDir:' . $settingFile);
            }
        }
        elseif ($this->isValidModule($module)) {
            $isAlias = false;
        }
        else {
            $isAlias = false;
            if ($this->isValidModule($this->_defaultModule)) {
                $module = $this->_defaultModule;
                if (false === $dryrun) {
                    $request->setModuleName($module);
                }
            } else {
                require_once 'Zend/Controller/Exception.php';
                throw new Zend_Controller_Exception('No default module defined for this application');
            }
        }

        /**
         * ここから、実コントローラーが読み込み可能かをチェックする。
         * $curDirectory 現在のディレクトリとしてコントローラー等から参照される
         * $dispatchDir
         */
        if ($isAlias) {
            /**
             * 現在のディレクトリとしてコントローラー等から参照される
             * @var string $curDirectory
             */
            $curDirectory       = $aliasDirs[$module];
            /**
             * チェック対象のディレクトリ。実コントローラが格納されている
             * @var string $dispatchDir
             */
            $dispatchDir        = $this->getControllerDirectory($realModule);
            $controllerName     = $realController;
            $curControllerClass = $this->formatControllerName($controllerName);
        }
        else {
            $curDirectory       = $realDirs[$module];
            $dispatchDir        = $curDirectory;
            $controllerName     = $request->getControllerName();
            $curControllerClass = $this->formatControllerName();
        }
        $fileSpec    = $this->classToFilename($curControllerClass);
        $test = $dispatchDir . DIRECTORY_SEPARATOR . $fileSpec;
        if (!Zend_Loader::isReadable($test)) {
            if (!$this->getParam('useDefaultControllerAlways') && !empty($controllerName)) {
                require_once 'Zend/Controller/Dispatcher/Exception.php';
                throw new Zend_Controller_Dispatcher_Exception('Invalid controller specified (' . $controllerName . ':in '. $test .')');
            }
			$controllerName = $this->getDefaultControllerName();
			$curControllerClass = $this->formatControllerName($controllerName);
			if (false === $dryrun) {
	            $request->setControllerName($controllerName)
	                ->setActionName(null);
			}
        }
        /**
         * dryrunでなければ、AliasDirのプロパティを書き換える
         */
        if (false === $dryrun) {
            $this->_curModule    = $module;
            $this->_curDirectory = $curDirectory;
            $this->_curControllerClass = $curControllerClass;
            $this->_isAlias = $isAlias;
            if ($isAlias) {
                $this->_aliasConf = $aliasConf;
                $this->_realModule = $realModule;
                $this->_realController = $realController;
            } else {
                $this->_aliasConf = null;
                $this->_realModule = null;
                $this->_realController = null;
            }
        }
        return true;
    }

こ、これは

いろいろとやってみたが、エイリアス転送時のバリエーションが多すぎる。
ある程度仕様を絞ったコントローラーだけエイリアス可能にする必要がありそう。ダブルディスパッチを利用して、AliasDirからdispatch可能な仕様をもったコントローラーでない場合は、単純なリダイレクトとする。
エイリアス設定しない。また、コントローラー側でも、エイリアス設定を許す対象を絞ることができるようにする。