# MVC での例外

## 導入

Zend Framework の MVC コンポーネントは、 フロントコントローラを使用しています。 つまり、あるサイトに対するすべてのリクエストを ひとつのエントリポイントで処理するということです。その結果、 すべての例外は最終的にフロントコントローラに到達することになります。 開発者は、例外をここでまとめて処理できます。

しかし、例外のメッセージやバックトレースの中には、 システムの重要な情報が含まれていることがあります。 たとえば SQL 文の内容やファイルの位置といった情報です。 あなたのサイトを守るため、デフォルトでは Zend_Controller_Front がすべての例外を捕捉し、 それをレスポンスオブジェクトに登録するようになっています。 また、レスポンスオブジェクトは、 デフォルトではそれらの例外メッセージを表示しません。

## 例外の処理

MVC コンポーネント内で例外を処理するための仕組みが組み込まれています。

- デフォルトでは、 エラーハンドラプラグイン が登録され、有効になっています。 このプラグインは、次のようなエラーを処理するように設計されています。

  - コントローラやアクションが存在しない場合に発生するエラー

  - アクションコントローラ内で発生するエラー

  このプラグインは postDispatch() プラグインとして動作し、ディスパッチャやアクションコントローラで 他の例外が発生していないかどうかを調べます。発生していた場合は、 エラーハンドラコントローラに処理を転送します。

  このハンドラは大半の例外状況をカバーし、 コントローラやアクションが存在しない場合の対応を行います。

- Zend_Controller_Front::throwExceptions()

  このメソッドに `TRUE` を渡すと、 フロントコントローラがレスポンスオブジェクトに例外をまとめたり エラーハンドラプラグインを使用したりするかわりに 例外を自分自身で処理できるようになります。たとえば次のようになります。

      $front->throwExceptions(true);
      try {
          $front->dispatch();
      } catch (Exception $e) {
          // ここで、自分自身で例外を処理します
      }

  これが、自分のアプリケーションで独自の例外処理を行うための もっとも簡単な方法でしょう。 この方法で、発生しうるすべての例外を処理できます。

- Zend_Controller_Response_Abstract::renderExceptions()

  このメソッドに `TRUE` を渡すと、 レスポンスオブジェクトをレンダリングする際に 例外メッセージやバックトレースも表示するようになります。 これを行うと、アプリケーションで発生したすべての例外が表示されるようになります。 実際の運用環境以外でのみ使用するようにしましょう。

- Zend_Controller_Front::returnResponse() および Zend_Controller_Response_Abstract::isException()

  Zend_Controller_Front::returnResponse() に `TRUE` を渡すと、 Zend_Controller_Front::dispatch() はレスポンスをレンダリングせず、 そのまま返します。レスポンスを受け取った後で、 処理すべき例外があるかどうかを isException() メソッドで調べ、その内容を getException() メソッドで取得します。 たとえば次のようになります。

      $front->returnResponse(true);
      $response = $front->dispatch();
      if ($response->isException()) {
          $exceptions = $response->getException();
          // 例外を処理します ...
      } else {
          $response->sendHeaders();
          $response->outputBody();
      }

  Zend_Controller_Front::throwExceptions() に比べてこの方法が優れている点は、例外を処理した後で、 それをレンダリングするかどうかを判断できるところです。 エラーハンドラプラグインとは異なり、 これはコントローラチェイン内で発生したすべての例外を捕捉します。

## MVC で遭遇するであろう例外

各 MVC コンポーネント群 -- リクエスト、ルータ、ディスパッチャ、 アクションコントローラそしてレスポンスオブジェクト -- はそれぞれ例外をスローします。 条件によっては上書きされる例外もありますし、 中には開発者がアプリケーションの構造を見直さなければならないような例外もあるでしょう。

いくつか例を示します。

- Zend_Controller_Dispatcher::dispatch() は、デフォルトでは、 無効なコントローラがリクエストされた際に例外をスローします。 この例外への対処方法としては、次のふたつを推奨します。

  - パラメータ useDefaultControllerAlways を設定します。

    フロントコントローラかディスパッチャのいずれかで、 以下のディレクティブを追加します。

        $front->setParam('useDefaultControllerAlways', true);

        // あるいは

        $dispatcher->setParam('useDefaultControllerAlways', true);

    このフラグを設定すると、 ディスパッチャは例外をスローせず、 かわりにデフォルトのコントローラとアクションを使用するようになります。 この方式の欠点は、ユーザがサイトのアドレスを打ち間違えた際にも ホームページが表示されてしまうことです。これは サーチエンジン最適化を台無しにしてしまう可能性があります。

  - dispatch() がスローする例外は Zend_Controller_Dispatcher_Exception で、この中には 'Invalid controller specified' というテキストが含まれます。 先ほどの節 でとりあげられているメソッドのいずれかでこの例外を捕捉し、 共通のエラーページあるいはホームページにリダイレクトします。

- Zend_Controller_Action::__call() は、存在しないアクションを メソッドにディスパッチできなかった場合に Zend_Controller_Action_Exception をスローします。 このような場合は、何らかのデフォルトアクションを コントローラで使用したいことでしょう。そのためには次のようにします。

  - Zend_Controller_Action のサブクラスを作成し、 __call() メソッドをオーバーライドします。 たとえば次のようになります。

        class My_Controller_Action extends Zend_Controller_Action
        {
            public function __call($method, $args)
            {
                if ('Action' == substr($method, -6)) {
                    $controller = $this->getRequest()->getControllerName();
                    $url = '/' . $controller . '/index';
                    return $this->_redirect($url);
                }

                throw new Exception('Invalid method');
            }
        }

    上の例は、未定義のアクションメソッドがコールされた場合にそれをすべて受け取り、 そのコントローラのデフォルトアクションにリダイレクトします。

  - Zend_Controller_Dispatcher のサブクラスを作成し、 getAction() メソッドをオーバーライドして、 アクションが存在するかどうかを調べます。 たとえば次のようになります。

        class My_Controller_Dispatcher extends Zend_Controller_Dispatcher
        {
            public function getAction($request)
            {
                $action = $request->getActionName();
                if (empty($action)) {
                    $action = $this->getDefaultAction();
                    $request->setActionName($action);
                    $action = $this->formatActionName($action);
                } else {
                    $controller = $this->getController();
                    $action     = $this->formatActionName($action);
                    if (!method_exists($controller, $action)) {
                        $action = $this->getDefaultAction();
                        $request->setActionName($action);
                        $action = $this->formatActionName($action);
                    }
                }

                return $action;
            }
        }

    上のコードは、指定したアクションが そのコントローラクラスに存在するかどうかを調べます。 存在しない場合は、デフォルトのアクションにリセットします。

    この方式の利点は、最終的にディスパッチが行われる前に 透過的にアクションを変更できるとうことです。しかしこれは、 URL を打ち間違えた際にも正しくディスパッチされてしまうということでもあります。 これは、サーチエンジン最適化のためにはあまりよくありません。

  - Zend_Controller_Action::preDispatch() あるいは Zend_Controller_Plugin_Abstract::preDispatch() を使用して、無効なアクションを判別します。

    Zend_Controller_Action のサブクラスを作成して preDispatch() を変更することで、 実際にアクションをディスパッチする前に コントローラ内で別のアクションに転送したり リダイレクトしたりすることが可能となります。 このコードは、先ほど説明した __call() をオーバーライドするコードと似たものになります。

    もうひとつの方法としては、 この情報をグローバルプラグインで調べることもできます。 この方式の利点は、アクションコントローラとは独立しているというところです。 アプリケーション内でさまざまなアクションコントローラを使用しているとしましょう。 それらがすべて同一のクラスを継承しているとは限りません。 そのような場合にこの方式を使用すると、 さまざまなクラスに対して一貫した処理を行えます。

    たとえば次のようになります。

        class My_Controller_PreDispatchPlugin extends Zend_Controller_Plugin_Abstract
        {
            public function preDispatch(Zend_Controller_Request_Abstract $request)
            {
                $front      = Zend_Controller_Front::getInstance();
                $dispatcher = $front->getDispatcher();
                $class      = $dispatcher->getControllerClass($request);
                if (!$class) {
                    $class = $dispatcher->getDefaultControllerClass($request);
                }

                $r      = new ReflectionClass($class);
                $action = $dispatcher->getActionMethod($request);

                if (!$r->hasMethod($action)) {
                    $defaultAction  = $dispatcher->getDefaultAction();
                    $controllerName = $request->getControllerName();
                    $response       = $front->getResponse();
                    $response->setRedirect('/' . $controllerName
                                          . '/' . $defaultAction);
                    $response->sendHeaders();
                    exit;
                }
            }
        }

    この例では、 リクエストされたアクションがそのコントローラに存在するかどうかを調べます。 存在しない場合は、そのコントローラのデフォルトアクションにリダイレクトします。 そしてそこでスクリプトの実行を終了します。