チャット作るよ。6日目

アクションを作ったので、制御するコントローラを作る。

まずはテストケース。

<?php
class ControllerTest extends PHPUnit_Extensions_OutputTestCase {

    /**
     * テストアプリディレクトリパス
     */ 
    const APP_DIR_PATH = '/project/chat/_test';
    
    /**
     * テストアクションディレクトリパス
     */
    const ACT_DIR_PATH = '/project/chat/_test/action';

    /**
     * Indexアクションファイルパス
     */
    const INDEX_ACT_FILE_PATH = '/project/chat/_test/action/IndexAction.php';

    /**
     * Index2アクションファイルパス
     */
    const INDEX2_ACT_FILE_PATH = '/project/chat/_test/action/Index2Action.php';

    /**
     * Emptyアクションファイルパス
     */
    const EMPTY_ACT_FILE_PATH = '/project/chat/_test/action/EmptyAction.php';

    public static function setUpBeforeClass() {
        // テスト用ディレクトリ、及びファイルの生成
        mkdir(self::ACT_DIR_PATH, 0777, true);
        $index = array();
        $index[] = '<?php';
        $index[] = 'class IndexAction extends Action {';
        $index[] = 'public function run() {';
        $index[] = 'print("success");';
        $index[] = '}';
        $index[] = '}';
        file_put_contents(self::INDEX_ACT_FILE_PATH, implode("\n", $index));
        $index2 = array();
        $index2[] = '<?php';
        $index2[] = 'class Index2Action extends Action {';
        $index2[] = 'public function run() {';
        $index2[] = 'print("test");';
        $index2[] = '}';
        $index2[] = '}';
        file_put_contents(self::INDEX2_ACT_FILE_PATH, implode("\n", $index2));
        touch(self::EMPTY_ACT_FILE_PATH);
    }

    public function setUp() {
        $this->option = array(
            'appDirPath' => self::APP_DIR_PATH
        );
        $this->ctrl = Controller::getInstance($this->option);
    }

    public function testGetInstance() {
        $expect = Controller::getInstance($this->option);
        // 生成したインスタンスが同じであること
        $this->assertEquals($this->ctrl, $expect);
    }

    public function testRun() {
        // 指定したアクションが実行されること
        $this->expectOutputString('test');
        $this->ctrl->run('index2');
    }
    
    public function testRunDefault() {
        // アクションを指定しない場合に、デフォルトが実行されること
        $this->expectOutputString('success');
        $this->ctrl->run();
    }
    
    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testBadParamError() {
        // 半角英数字でない文字を含むアクション名はエラーになること
        $this->expectOutputString('404 Not Found.');
        $this->ctrl->run('../../../../../../../../../etc/passwd%00');
    }
    
    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testFileNotFoundError() {
        // アクションクラスファイルが見つからない場合エラーになること
        $this->expectOutputString('404 Not Found.');
        $this->ctrl->run('none');
    }

    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testClassNotFoundError() {
        // クラスが見つからない場合エラーになること
        $this->expectOutputString('404 Not Found.');
        $this->ctrl->run('empty');
    }

    public static function tearDownAfterClass() {
        // テスト用ファイル及びディレクトリを削除
        unlink(self::INDEX_ACT_FILE_PATH);
        unlink(self::INDEX2_ACT_FILE_PATH);
        unlink(self::EMPTY_ACT_FILE_PATH);
        rmdir(self::ACT_DIR_PATH);
        rmdir(self::APP_DIR_PATH);
    }
    
}

続いてソース。

<?php
/**
 * コントローラ
 *
 */
class Controller {

    /**
     * デフォルトアクション
     */
    const DEFAULT_ACTION = 'index';

    /**
     * インスタンス
     */
    private static $_instance = null;

    /**
     * アプリケーションディレクトリパス
     */
    private $_appDirPath;

    /**
     * コンストラクタ
     *
     * @param array $option 初期化パラメータ
     */ 
    private function Controller($option) {
        // アプリケーションディレクトリパス
        $this->_appDirPath = $option['appDirPath'];
    }

    /**
     * コントローラのインスタンスを取得します。
     *
     * @param array $option 初期化パラメータ
     * @return Controller コントローラインスタンス
     */ 
    public static function getInstance($option) {
        // Singleton
        if(self::$_instance === null) {
            self::$_instance = new Controller($option);
        }
        return self::$_instance;
    }

    /**
     * 指定アクションを実行します。
     *
     * @param string $actionName アクション名
     * @return void 
     */
    public function run($actionName = null) {
        if($actionName === null) {
            // アクション名指定なしの場合、デフォルトにする
            $actionName = self::DEFAULT_ACTION;
        }
        // アクションインスタンスを生成
        $action = $this->_loadAction($actionName);
        // アクション初期化設定
        $config = array(
            'appDirPath' => $this->_appDirPath
        );
        // アクションを初期化
        $action->initialize($config);
        // アクションを実行
        $action->run();    
    }

    /**
     * 指定アクションのインスタンスを生成します。
     *
     * @param string $actionName アクション名
     * @return Action アクションインスタンス
     */
    private function _loadAction($actionName) {
        try {
            if(preg_match("/\W/", $actionName)) {
                // 半角英数字以外が含まれた場合、エラーにする
                throw new Exception("Bad Parameter. [$actionName]");
            }
            // アクションクラス名を作成
            $clazz = ucfirst($actionName) . 'Action';
            if(!class_exists($clazz)) {
                // クラスが存在しない場合、ファイルパスを作成
                $path = $this->_appDirPath . "/action/$clazz.php";
                if(!file_exists($path)) {
                    // ファイルが存在しない場合、エラーにする
                    throw new Exception("File Not Found. [$path]");
                }
                // ファイル読み込み
                require_once($path);
                if(!class_exists($clazz)) {
                    // クラスが存在しない場合、エラーにする
                    throw new Exception("Action Not Found. [$clazz]");
                }
            }
            // インスタンスを生成して返却
            $action = new $clazz;
            return $action;
        } catch(Exception $e) {
            // エラー処理
            header('HTTP/1.1 404 Not Found', true);
            print('404 Not Found.');
            trigger_error($e->getMessage(), E_USER_ERROR);
            exit();
        }
    }
}

エラーハンドリングがちょっと適当。とりあえず暫定対応。


HudsonとPhing使ってビルド環境作ってみてたら、今までのテストケース結構手を入れてしまった。
テスト内容は変わってないからまあいいかと。ビルドも通ったし。
そろそろアプリの実装に入ろう。