7.3. Subclassing

7.3.1. Introduction

The Zend_Controller system was built with extensibility in mind, either by subclassing the existing classes or writing new classes that implement the interfaces Zend_Controller_Router_Interface and Zend_Controller_Dispatcher_Interface or extend the classes Zend_Controller_Request_Abstract, Zend_Controller_Response_Abstract, and Zend_Controller_Action.

Possible reasons for subclassing include:

  • The existing URI routing system is not suitable in some way, such as integrating into an existing website that uses its own conventions for routing that do not mesh with the routing mechanism provided with the Zend Framework.

  • You need to implement routing for something completely different. The Zend_Controller_Router class only deals with URIs. It's possible and likely that you might want to use the MVC pattern for developing another type of program, such as a console or GUI application. In the case of a console application, a custom request object could process command line arguments.

  • The mechanism provided by Zend_Controller_Dispatcher is not suitable. The default configuration assumes a convention that controllers are classes and actions are methods of those classes. However, there are many other strategies for doing this. One example would be where controllers are directories and actions are files within those directories.

  • You wish to provide additional capabilities that will be inherited by all of your controllers. For example, Zend_Controller_Action does not integrate with Zend_View by default. However, you could extend your own controller to do this, and using it would not require modifying the supplied Zend_Controller_Router or Zend_Controller_Dispatcher.

  • You wish to log application exceptions when caught and redirect to a generic error page. Extending Zend_Controller_Response_Http, you could modify __toString() to check for registered exceptions, log them, and then redirect to an error page.

Please be careful when overriding significant parts of the system, particularly the dispatcher. One of the advantages of Zend_Controller is that it establishes common conventions for building applications. If too much of this default behavior is changed, some of these advantages are lost. However, there are many different needs and one solution can't fit them all, so the freedom is provided if needed.

7.3.2. Conventions

When subclassing any of the Zend_Controller classes, you are strongly encouraged to follow these conventions for naming or storing files. Doing so will ensure that another programmer who is familiar with the Zend Framework will be able to understand your project easily.

7.3.2.1. Prefix

Classes included with the Zend Framework follow a convention where every class is prefixed by "Zend_". This is the prefix. We recommend that you name all of your classes in the same way, e.g. if your company name is Widget, Inc., the prefix might be "Widget_".

7.3.2.2. Directory Layout

The Zend_Controller classes are stored in the library directory as follows:

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

When subclassing Zend_Controller classes, it is recommended that the new classes be stored in an identical structure under your prefix. This will make them easy to find for someone during that learning process of reviewing the code for your project.

For example, a project from Widget, Inc. which implements only a custom router might look like this:

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

Notice in this example that the Widget/Controller/ directory mirrors the Zend/Controller/ directory wherever possible. In this case, it provides the class Widget_Controller_Router, which would be either a subclass or replacement for Zend_Controller_Router implementing Zend_Controller_Router_Interface.

Also, notice in the example above that a README.txt file has been placed in Widget/Controller/. Zend strongly encourages you to document your projects through supplying separate tests and documentation for customers. However, we encourage you to also place a simple README.txt file right in the directory to briefly explain your changes and how they work.

7.3.3. Front Controller

Zend_Controller_Front implements a Front Controller. Additionally, it is a singleton class, meaning only one instance of it may be available at any given point.

To subclass it, at the very minimum you will need to override the getInstance() method:

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

        return self::$_instance;
    }
}

Overriding the getInstance() method ensures that subsequent calls to Zend_Controller_Front::getInstance() will return an instance of your new subclass instead of a Zend_Controller_Front instance -- this is particularly useful for some of the alternate routers and view helpers.

In addition to getInstance(), there are many other methods you may override:

    /**
     * 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);

The purpose of the front controller is to setup the request environment, route the incoming request, and then dispatch any discovered actions. Finally, it aggregates any responses and returns them.

The main reasons to extend the front controller would be to change the logic for one of the accessor methods (for instance, to load a different default router or dispatcher, or to specify different logic for how controller directories are handled), or to change how routing and dispatching occur.

7.3.4. Request Abstract

The abstract Zend_Controller_Request_Abstract defines a handful of methods:

    /**
     * @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();
}

The request object is a container for the request environment. The controller chain really only needs to know how to set and retrieve the controller, action, optional parameters, and dispatched status. By default, the request will search its own parameters using the controller or action keys in order to determine the controller and action.

7.3.5. Router Interface

The interface Zend_Controller_Router_Interface provides a definition for only one method:

<?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);

?>

Routing only occurs once: when the request is first received into the system. The purpose of the router is to determine the controller, action, and optional parameters based on the request environment, and then set them in the request. The request object is then passed to the dispatcher. If it is not possible to map a route to a dispatch token, the router should do nothing to the request object.

7.3.6. Dispatcher Interface

Zend_Controller_Front will first call the router to determine the first dispatchable action in the request. It then enters a dispatch loop.

In the loop, it first sets the request object's dispatched flag and then dispatches the request (instantiate the controller, call its action). If the action method (or a pre/postDispatch plugin) resets the request object's dispatched flag, the front controller will do another iteration of the dispatch loop with whatever action is currently set in the request object. This allows for actions to be processed sequentially until all work has been done.

The interface Zend_Controller_Dispatcher_Interface provides definitions for two methods:

<?php

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

?>

isDispatchable() checks if a request is dispatchable. If it is, it returns TRUE. Otherwise, it returns FALSE. The definition of what is dispatchable is left to the class implementing the interface. In the case of the default implementation, Zend_Controller_Dispatcher, it means that the controller file exists, the class exists within the file, and the action method exists within the class.

<?php

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

?>

dispatch() is where the work gets done. This method must execute the action of the controller. It must return a request object.

7.3.7. Action Controller

The Action Controller handles the various actions of an application. This abstract class provides the following methods:

    /**
     * @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);

The constructor registers the request and response objects with the object, as well as an array of any additional configuration arguments. This last array consists of parameters registered with the Front Controller's setParam() or setParams() methods. Once done, the constructor passes handling to init().

While you may override the constructor, we suggest putting any initialization handling into init() to ensure the request and response objects are properly registered.

Any configuration arguments passed to the constructor are later accessible using getInvokeArg() and getInvokeArgs(). The recommendation is to use such invocation arguments to pass in objects such as view, authentication/authorization, or registry objects. For example:

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

// In a sample action controller:
class FooController extends Zend_Controller_Action
{
    protected $_view = null;

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

When an action is dispatched, processing may be performed before and after the action using the preDispatch() and postDispatch() methods, respectively. By default, they are empty and do nothing.

The __call() method will handle any unregistered actions in the class. By default, it throws an exception if the action is not defined. This should only ever occur if the default action method is not defined.

The default naming convention for action methods is lowercaseAction, where 'lowercase' specifies the name of the action, and 'Action' specifies that the method is an action method. Thus, http://framework.zend.com/foo/bar will call FooController::barAction().

Action controllers may also be used as Page Controllers. Most typical usage would be as follows:

$controller = new FooController(
    new Zend_Controller_Request_Abstract(),
    new Zend_Controller_Response_Abstract()
);
$controller->run();
[Nota] Use Front-/Action Controller

We recommend using the Front Controller/Action Controller combination instead of the Page Controller paradigm to encourage writing applications that will inter-operate.

7.3.8. Response Object

The Response Object collects content and headers from the various actions called and returns them to the client. It has the following methods:

    /**
     * @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() will replace all body content; we encourage using appendBody() instead. __toString() should render any content and send all headers.

The response object is also where action controller exceptions are finally caught and registered (unless Zend_Controller_Front::throwExceptions() has been enabled. isException() should return a boolean indicating whether or not this has occurred. renderExceptions() should be used to indicate whether __toString() will render exception output if an exception was trapped.