7.3. サブクラス化

7.3.1. 導入

Zend_Controller システムは、拡張性を考慮して作成されています。 拡張方法としては、既存のクラスを継承する以外にも Zend_Controller_Router_Interface および Zend_Controller_Dispatcher_Interface を実装した新しいクラスを作成する方法があります。また、 Zend_Controller_Request_AbstractZend_Controller_Response_Abstract そして Zend_Controller_Action を継承したクラスを作成することもできます。

クラスを拡張する理由としては、以下のようなものが考えられるでしょう。

  • 既存の URI ルーティングが何らかの理由で適用できない場合。 例えば、既存のウェブサイトと統合する予定であるが、 そこで採用している規約が Zend Framework のルーティング機構と合致しないなど。

  • まったく別のルーティングを実装しなければならない場合。 Zend_Controller_Router クラスは URI のみを対象としています。 例えばコンソールアプリケーションや GUI アプリケーションなどの別の形式のプログラムにも MVC パターンを使用したくなることもあるでしょう。 コンソールアプリケーションの場合なら、 独自のルータを実装すればコマンドライン引数によるルーティングを行うこともできます。

  • Zend_Controller_Dispatcher の提供する機能がうまく当てはまらない場合。 デフォルト設定では、コントローラはクラスであり、 アクションはクラスのメソッドであることを前提としています。 しかし、これ以外にもいろいろなやり方があるでしょう。 例えば、ディレクトリをコントローラ、 ディレクトリ内のファイルをアクションと考えることもできます。

  • すべてのコントローラで使用するための機能を追加したい場合。 例えば、デフォルトでは Zend_Controller_ActionZend_View と統合されていません。しかし、 コントローラを継承したクラスでこの機能を持たせることができます。 そうすれば、もとの Zend_Controller_RouterZend_Controller_Dispatcher に手を加えずにすみます。

  • アプリケーションで発生した例外を記録し、 共通のエラーページにリダイレクトさせたい場合。既存の Zend_Controller_Response_Http を拡張し、 __toString() を変更することで、 登録された例外のチェックとその記録、 エラーページへのリダイレクトが可能となります。

システムの主要な部分、特にディスパッチャをオーバーライドする際には十分注意してください。 Zend_Controller を使用する利点のひとつに、 共通の規約に従ったアプリケーションを作成できるということがあります。 デフォルトの振る舞いをあまりにも変更してしまうと、この利点が失われてしまいます。 しかしながら、世の中にはさまざまな需要があるわけですし、 ひとつのソリューションですべて解決できるわけがありません。 そのために、必要なら変更できるようにしています。

7.3.2. 規約

Zend_Controller のクラスを継承する場合は、 その命名や保存場所はできるだけ次の規約に従うようにしてください。 そうすることで、Zend Framework になじみのある他のプログラマが、 あなたのプロジェクトの内容を簡単に理解できるようになります。

7.3.2.1. プレフィックス

Zend Framework に含まれるクラスの名前は、すべて "Zend_" で始まっています。これがプレフィックスです。 あなたが作成するクラスも同じようにしておくことを推奨します。 例えば、もしあなたの所属する会社名が Widget, Inc. なら、 プレフィックスを "Widget_" とします。

7.3.2.2. ディレクトリ構成

Zend_Controller クラスは、 ライブラリディレクトリに以下のように格納されます。

/library
  /Zend
    /Controller
      Action.php
      Dispatcher.php
      Router.php

Zend_Controller クラスを継承する場合は、 作成したクラスを (あなたのプレフィックスのもとで) 同じ構造で格納することを推奨します。こうすることで、 あなたのプロジェクトのコードをレビューする人たちがそれを見つけやすくなるでしょう。

例えば Widget, Inc. のプロジェクトがルータのみを独自に実装した場合は、 次のようになるでしょう。

/library
  /Zend
  /Widget
    /Controller
      Router.php
      README.txt

この例では、Widget/Controller/ ディレクトリが Zend/Controller/ ディレクトリと同じ構造になっていることに注意しましょう。 この場合、このファイルでは Widget_Controller_Router というクラスが提供されます。このクラスは Zend_Controller_Router を継承したものか、あるいは Zend_Controller_Router_Interface を実装した独自のクラスとなります。

また、上の例で Widget/Controller/ ディレクトリに README.txt があることにも注意してください。 あなたのプロジェクトを顧客に提供する際には、 それに関する単体テストや文書も作成するよう、Zend は強く推奨します。 そこまではいかなくても、同じディレクトリにシンプルな README.txt ファイルを置き、 変更点や動作内容を簡単に説明しておくようにしましょう。

7.3.3. フロントコントローラ

Zend_Controller_Front は、フロントコントローラを実装したものです。 さらに、これはシングルトンクラスでもあります。 つまり、どの時点においてもインスタンスはひとつだけしか存在しないということです。

このサブクラスを作成するために最低限必要なことは、 getInstance() メソッドをオーバーライドすることです。

class My_Controller_Front extends Zend_Controller_Front
{
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }
}

getInstance() メソッドをオーバーライドすることで、 Zend_Controller_Front::getInstance() のコール時に Zend_Controller_Front ではなく そのサブクラスのインスタンスを返すようにします。 これにより、ルータやビューヘルパーの挙動を変更することができます。

getInstance() 以外にも、 他の多くのメソッドをオーバーライドできます。

    /**
     * Resets all object properties of the singleton instance
     *
     * Primarily used for testing; could be used to chain front controllers.
     * 
     * @return void
     */
    public function resetInstance();

    /**
     * Convenience feature, calls setControllerDirectory()->setRouter()->dispatch()
     *
     * In PHP 5.1.x, a call to a static method never populates $this -- so run() 
     * may actually be called after setting up your front controller.
     *
     * @param string|array $controllerDirectory Path to Zend_Controller_Action 
     * controller classes or array of such paths
     * @return void
     * @throws Zend_Controller_Exception if called from an object instance
     */
    public static function run($controllerDirectory);

    /**
     * Add a controller directory to the controller directory stack
     *
     * If $args is presented and is a string, uses it for the array key mapping 
     * to the directory specified.
     * 
     * @param string $directory 
     * @param mixed $args Optional argument; if string value, used as array key map
     * @return Zend_Controller_Front
     */
    public function addControllerDirectory($directory, $args = null);

    /**
     * Set controller directory
     *
     * Stores controller directory to pass to dispatcher. May be an array of 
     * directories or a string containing a single directory.
     *
     * @param string|array $directory Path to Zend_Controller_Action controller 
     * classes or array of such paths
     * @return Zend_Controller_Front
     */
    public function setControllerDirectory($directory);

    /**
     * Retrieve controller directory
     *
     * Retrieves stored controller directory
     *
     * @return string|array
     */
    public function getControllerDirectory();

    /**
     * Set the default controller (unformatted string)
     *
     * @param string $controller
     * @return Zend_Controller_Front
     */
    public function setDefaultController($controller);

    /**
     * Retrieve the default controller (unformatted string)
     *
     * @return string
     */
    public function getDefaultController();

    /**
     * Set the default action (unformatted string)
     *
     * @param string $action
     * @return Zend_Controller_Front
     */
    public function setDefaultAction($action);

    /**
     * Retrieve the default action (unformatted string)
     *
     * @return string
     */
    public function getDefaultAction();

    /**
     * Set request class/object
     *
     * Set the request object.  The request holds the request environment.
     *
     * If a class name is provided, it will instantiate it
     *
     * @param string|Zend_Controller_Request_Abstract $request
     * @throws Zend_Controller_Exception if invalid request class
     * @return Zend_Controller_Front
     */
    public function setRequest($request);

    /**
     * Return the request object.
     *
     * @return null|Zend_Controller_Request_Abstract
     */
    public function getRequest();

    /**
     * Set router class/object
     *
     * Set the router object.  The router is responsible for mapping
     * the request to a controller and action.
     *
     * If a class name is provided, instantiates router with any parameters
     * registered via {@link setParam()} or {@link setParams()}.
     *
     * @param string|Zend_Controller_Router_Interface $router
     * @throws Zend_Controller_Exception if invalid router class
     * @return Zend_Controller_Front
     */
    public function setRouter($router);

    /**
     * Return the router object.
     *
     * Instantiates a Zend_Controller_Router object if no router currently set.
     *
     * @return null|Zend_Controller_Router_Interface
     */
    public function getRouter();

    /**
     * Set the base URL used for requests
     *
     * Use to set the base URL segment of the REQUEST_URI to use when 
     * determining PATH_INFO, etc. Examples:
     * - /admin
     * - /myapp
     * - /subdir/index.php
     *
     * Note that the URL should not include the full URI. Do not use:
     * - http://example.com/admin
     * - http://example.com/myapp
     * - http://example.com/subdir/index.php
     *
     * If a null value is passed, this can be used as well for autodiscovery (default).
     * 
     * @param string $base
     * @return Zend_Controller_Front
     * @throws Zend_Controller_Exception for non-string $base
     */
    public function setBaseUrl($base = null);

    /**
     * Retrieve the currently set base URL
     * 
     * @return string
     */
    public function getBaseUrl();

    /**
     * Set the dispatcher object.  The dispatcher is responsible for
     * taking a Zend_Controller_Request_Abstract object, instantiating the controller, and
     * calling the action method of the controller.
     *
     * @param Zend_Controller_Dispatcher_Interface $dispatcher
     * @return Zend_Controller_Front
     */
    public function setDispatcher(Zend_Controller_Dispatcher_Interface $dispatcher);

    /**
     * Return the dispatcher object.
     *
     * @return Zend_Controller_DispatcherInteface
     */
    public function getDispatcher();

    /**
     * Set response class/object
     *
     * Set the response object.  The response is a container for action
     * responses and headers. Usage is optional.
     *
     * If a class name is provided, instantiates a response object.
     *
     * @param string|Zend_Controller_Response_Abstract $response
     * @throws Zend_Controller_Exception if invalid response class
     * @return Zend_Controller_Front
     */
    public function setResponse($response);

    /**
     * Return the response object.
     *
     * @return null|Zend_Controller_Response_Abstract
     */
    public function getResponse();

    /**
     * Add or modify a parameter to use when instantiating an action controller
     *
     * @param string $name
     * @param mixed $value
     * @return Zend_Controller_Front
     */
    public function setParam($name, $value);

    /**
     * Set parameters to pass to action controller constructors
     *
     * @param array $params
     * @return Zend_Controller_Front
     */
    public function setParams(array $params);

    /**
     * Retrieve a single parameter from the controller parameter stack
     * 
     * @param string $name 
     * @return mixed
     */
    public function getParam($name);

    /**
     * Retrieve action controller instantiation parameters
     *
     * @return array
     */
    public function getParams();

    /**
     * Clear the controller parameter stack
     *
     * By default, clears all parameters. If a parameter name is given, clears 
     * only that parameter; if an array of parameter names is provided, clears 
     * each.
     * 
     * @param null|string|array single key or array of keys for params to clear
     * @return Zend_Controller_Front
     */
    public function clearParams($name = null);

    /**
     * Register a plugin.
     *
     * @param Zend_Controller_Plugin_Abstract $plugin
     * @return Zend_Controller_Front
     */
    public function registerPlugin(Zend_Controller_Plugin_Abstract $plugin);

    /**
     * Unregister a plugin.
     *
     * @param Zend_Controller_Plugin_Abstract $plugin
     * @return Zend_Controller_Front
     */
    public function unregisterPlugin(Zend_Controller_Plugin_Abstract $plugin);

    /**
     * Set whether exceptions encounted in the dispatch loop should be thrown 
     * or caught and trapped in the response object
     *
     * Default behaviour is to trap them in the response object; call this 
     * method to have them thrown.
     * 
     * @param boolean $flag Defaults to true
     * @return boolean Returns current setting
     */
    public function throwExceptions($flag = null);

    /**
     * Set whether {@link dispatch()} should return the response without first 
     * rendering output. By default, output is rendered and dispatch() returns 
     * nothing.
     * 
     * @param boolean $flag 
     * @return boolean Returns current setting
     */
    public function returnResponse($flag = null);

    /**
     * Dispatch an HTTP request to a controller/action.
     *
     * @param Zend_Controller_Request_Abstract|null $request
     * @param Zend_Controller_Response_Abstract|null $response
     * @return void|Zend_Controller_Response_Abstract Returns response object if returnResponse() is true
     */
    public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null);

フロントコントローラの目的は、 リクエスト環境を設定すること、やってくるリクエストをルーティングすること、 アクションに応じて処理を振り分けること、そしてレスポンスを取得してそれを返すことです。

フロントコントローラを拡張する主な理由としては、 アクセス用メソッドのロジックを変更 (たとえば別のデフォルトルータやディスパッチャを読み込んだり コントローラディレクトリの処理方法を指定するなど) したり、ルーティングやディスパッチ処理を変更したりといったことがあります。

7.3.4. リクエストの抽象化

抽象クラス Zend_Controller_Request_Abstract で、いくつかのメソッドを定義しています。

    /**
     * @return string
     */
    public function getControllerName();

    /**
     * @param string $value 
     * @return self
     */
    public function setControllerName($value);

    /**
     * @return string
     */
    public function getActionName();

    /**
     * @param string $value 
     * @return self
     */
    public function setActionName($value);

    /**
     * @return string
     */
    public function getControllerKey();

    /**
     * @param string $key 
     * @return self
     */
    public function setControllerKey($key);

    /**
     * @return string
     */
    public function getActionKey();

    /**
     * @param string $key 
     * @return self
     */
    public function setActionKey($key);

    /**
     * @param string $key 
     * @return mixed
     */
    public function getParam($key);

    /**
     * @param string $key 
     * @param mixed $value 
     * @return self
     */
    public function setParam($key, $value);

    /**
     * @return array
     */
     public function getParams();

    /**
     * @param array $array 
     * @return self
     */
    public function setParams(array $array);

    /**
     * @param boolean $flag 
     * @return self
     */
    public function setDispatched($flag = true);

    /**
     * @return boolean
     */
    public function isDispatched();
}

リクエストオブジェクトは、リクエストのコンテナとなります。 コントローラチェインが必要とするのは、 コントローラ、アクション、オプションパラメータ およびディスパッチ状況を設定したり取得したりする方法だけとなります。 デフォルトでは、リクエストがパラメータを探す際には コントローラあるいはアクションのキーを使用します。 探す順序はコントローラおよびアクションで定義されます。

7.3.5. ルータのインターフェイス

Zend_Controller_Router_Interface で定義されているメソッドはひとつだけです。

<?php

  /**
   * @param  Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Router_Exception
   * @return Zend_Controller_Request_Abstract
   */
  public function route(Zend_Controller_Request_Abstract $request);

?>

ルーティングが発生するのは、システムが最初にリクエストを受け取ったときだけです。 ルータの役割は、コントローラやアクションそして リクエスト内容に応じたオプションパラメータを決定し、 それをリクエストに設定することです。 その後、リクエストオブジェクトをディスパッチャに渡します。 ルートをディスパッチャトークンに関連付けることはできません。 この場合、ルータはリクエストオブジェクトに対して何もしません。

7.3.6. ディスパッチャのインターフェイス

Zend_Controller_Front は、 まず最初にルータをコールして、 リクエスト内で最初にディスパッチできるアクションを決定します。 その後、ディスパッチャループに入ります。

ループ内では、まずリクエストオブジェクトでディスパッチフラグを設定し、 そしてリクエストを処理します (コントローラのインスタンスを作成し、 アクションをコールします)。アクションメソッド (あるいは pre/postDispatch プラグイン) がディスパッチフラグをリセットすると、リクエストオブジェクトの設定内容をもとにして フロントコントローラがループ内の次の処理を実行します。 これにより、すべてのアクションを順に実行していくことができます。

Zend_Controller_Dispatcher_Interface インターフェイスでは、 二つのメソッドが定義されています。

<?php

/**
 * @param  Zend_Controller_Request_Abstract $request
 * @return boolean
 */
public function isDispatchable(Zend_Controller_Request_Abstract $request);

?>

isDispatchable() は、 リクエストがディスパッチ可能かどうかを調べます。 ディスパッチ可能な場合に TRUE、それ以外の場合に FALSE を返します。ディスパッチ可能かどうかを判断する基準は、 インターフェイスを実装したクラスで記述します。 デフォルトの実装である Zend_Controller_Dispatcher では、 コントローラのファイルが存在するかどうか、 そのファイルでクラスが定義されているかどうか、 そしてそのクラスでアクションメソッドが定義されているかどうかを調べます。

<?php

/**
 * @param  Zend_Controller_Request_Abstract $route
 * @return Zend_Controller_Request_Abstract
 */
public function dispatch(Zend_Controller_Request_Abstract $request);

?>

dispatch() は、実際の処理を行うところです。 このメソッドは、コントローラのアクションを実行しなければなりません。 またリクエストオブジェクトを返す必要があります。

7.3.7. アクションコントローラ

アクションコントローラは、アプリケーションのさまざまなアクションを処理します。 この抽象クラスでは次のメソッドを提供します。

    /**
     * @param Zend_Controller_Request_Abstract $request Request object
     * @param Zend_Controller_Response_Abstract $response Response object
     * @param array $args Optional associative array of
     * configuration/environment settings
     */
    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $args = array());

    /**
     * @return void
     */
    public function init();

    /**
     * @return Zend_Controller_Request_Abstract
     */
    public function getRequest();

    /**
     * @param Zend_Controller_Request_Abstract $request 
     * @return self
     */
    public function setRequest(Zend_Controller_Request_Abstract $request);

    /**
     * @return Zend_Controller_Response_Abstract
     */
    public function getResponse();

    /**
     * @param Zend_Controller_Response_Abstract $response 
     * @return self
     */
    public function setResponse(Zend_Controller_Response_Abstract $response);

    /**
     * @return array
     */
    public function getInvokeArgs();

    /**
     * @return mixed
     */
    public function getInvokeArg($name);

    public function preDispatch();

    public function postDispatch();

    /**
     * @param string $methodName
     * @param array $args
     */
    public function __call($methodName, $args);

    /**
     * @param null|Zend_Controller_Request_Abstract $request Optional request 
     * object to use
     * @param null|Zend_Controller_Response_Abstract $response Optional response 
     * object to use
     * @return Zend_Controller_Response_Abstract
     */
    public function run(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null);

コンストラクタは、リクエストオブジェクトとレスポンスオブジェクトを登録し、 追加の設定引数の配列も登録します。この配列には、 フロントコントローラの setParam() メソッドや setParams() メソッドで登録されたパラメータが含まれます。 登録を済ませたあとで、コンストラクタは処理を init() に渡します。

コンストラクタをオーバーライドすることもできます。ただ、 初期化処理はすべて init() で行うようにし、 リクエストとレスポンスを適切に登録できるようにしておくことを推奨します。

コンストラクタに渡された設定引数へは、 getInvokeArg() および getInvokeArgs() でアクセスできるようになります。 お勧めの方法は、これらの起動時引数として ビューや認証情報、あるいはレジストリオブジェクトを渡すことです。 たとえば次のようになります。

$front = Zend_Controller_Front::getInstance();
$front->setParam('view', new Zend_View())
      ->setControllerDirectory($config->controller->directory);
$response = $front->dispatch();

// サンプルのアクションコントローラ
class FooController extends Zend_Controller_Action
{
    protected $_view = null;

    public function init()
    {
        $this->_view = $this->getInvokeArg('view');
    }
}

アクションがディスパッチされる際の前後に行われる処理が、それぞれ preDispatch() メソッドおよび postDispatch() メソッドです。 デフォルトではこれらのメソッドは空であり、何もしません。

__call() メソッドは、クラス内で登録されていないアクションを処理します。 デフォルトでは、これは未定義のアクションに対して例外をスローします。 これは、デフォルトのアクションメソッドが定義されていない場合にのみ発生します。

アクションメソッドのデフォルトの命名規約は lowercaseAction のようになります。'lowercase' の部分でアクション名を指定し、 'Action' の部分でこれがアクションメソッドであることを指定します。 つまり、http://framework.zend.com/foo/barFooController::barAction() をコールします。

アクションコントローラは、ページコントローラとして使用することもできます。 典型的な使用法は、このようになります。

$controller = new FooController(
    new Zend_Controller_Request_Abstract(),
    new Zend_Controller_Response_Abstract()
);
$controller->run();
[注意] フロントコントローラ/アクションコントローラの使用

ページコントローラ方式ではなく、 フロントコントローラ/ページコントローラ を組み合わせた方式を使用することを推奨します。 これにより、相互運用可能なアプリケーションを書けるようになります。

7.3.8. レスポンスオブジェクト

レスポンスオブジェクトは、コールされたアクションからの 内容やヘッダを収集し、それをクライアントに返します。 次のようなメソッドがあります。

    /**
     * @param string $name Header name
     * @param string $value Header value
     * @param boolean $replace Whether or not to replace headers with the same
     * name already registered with the object
     * @return self
     */
    public function setHeader($name, $value, $replace = false);

    /**
     * @return array
     */
    public function getHeaders();

    /**
     * @return void
     */
    public function clearHeaders();

    /**
     * Sends all headers
     * @return void
     */
    public function sendHeaders();

    /**
     * @param string $content
     * @return self
     */
    public function setBody($content);

    /**
     * @param string $content
     * @return self
     */
    public function appendBody($content);

    /**
     * @return string
     */
    public function getBody();

    /**
     * echoes body content
     * @return void
     */
    public function outputBody();

    /**
     * @param Exception $e 
     * @return self
     */
    public function setException(Exception $e);

    /**
     * @return null|Exception
     */
    public function getException();

    /**
     * @return boolean
     */
    public function isException();

    /**
     * @param boolean $flag
     * @return boolean
     */
    public function renderExceptions($flag = null);

    /**
     * @return string
     */
    public function __toString();

setBody() は、ボディ部の内容をすべて置き換えます。 このメソッドの代わりに appendBody() を使用することを推奨します。 __toString() は、内容をレンダリングしてすべてのヘッダを送信します。

レスポンスオブジェクトはまた、 アクションコントローラで発生した例外が最終的に捕捉、登録されるところでもあります (Zend_Controller_Front::throwExceptions() が有効になっている場合を除く)。 isException() は boolean 値を返します。 これは例外が発生したかどうかを表します。 renderExceptions() を使用すると、 例外を捕捉した際に __toString() が例外出力をレンダリングしたのかどうかがわかります。