Practical PHP Patterns: Front Controller
Join the DZone community and get the full member experience.
Join For FreeWe have seen that Page Controllers (or, with their alternate intent and name, Action Controllers), are the basic units that get to respond to HTTP requests. But since we have many Page Controllers, how can we distinguish between them and make everyone answer a different type of HTTP request?
The simplest solution is to associate every controller with a different end point: we do this naturally when we use PHP scripts. Every script is called when its path is the subject of the HTTP request - and the GET, POST parameters plus the various headers are passed to it as predefined variables.
In more complex use cases, however, it can be useful to decouple the routes (types of URL requested by the clients) from the Page Controllers. In this case, a layer that analyzes some parameters in the request has to be interposed.
Moreover, this layer can perform every operation that should be concentrated in a single point before the action-specific code is executed. Verifying authentication and authorization to access the single Page Controller is an example of a generic, centralized operation. Initialization of common resources like database connections or caches is another.
You may have noticed that, a while ago, PHP applications started to transition from a set of PHP scripts (which Wordpress still uses nowadays) to a single entry point (such as an index.php file, or an invisible index.php file which is called via Url rewriting, making paths like /category/4815162342 possible).
This central PHP script, or the class that is instanced and run in it, is called the Front Controller. The Front Controller handles every HTTP request which is routed to it via configuration of the web server, and decides to which entity the execution should be delegated. It can even be responsible for creating an abstraction over the plain PHP superglobal variables, like a Request object to contain parameters or a Response one to populate.
Input
Which parameters contained in an HTTP request does a Front Controller analyze? There is various implicit and explicit input data that can influence the behavior of the object.- The path of the virtual end point is usually the first considered parameter; for example the default route mechanism in the Zend Framework uses the schemes /controller/action and /module/controller/action.
- GET and POST parameters can be passed to the Page/Action Controllers but also used to route a request differently or to use a particular view. For example, the Zend Framework's Front Controller switches the format of the response basing on the format GET parameter. (?format=xml)
- HTTP headers can be analyzed, particularly when the Front Controller is managing a REST-like web service.
- Also custom HTTP headers (X-...) can be extracted from the request. A Zend Framework helper hooked in the Front Controller by default turns off the layout component basing on the presence of the X-Requested-With header, inserted in XMLHttpRequest objects by Ajax libraries. This way the main segment of a page can be returned for insertion in a page via Ajax without further configuration.
In general, HTTP is a rich protocol and its headers may convey useful information when a convention is adopted, in the limits of browser's capabilities.
Advantages and disadvantages (in PHP applications)
In PHP the Front Controller object is recreated at every HTTP request, with every collaborator object it may use. This means there are some peculiarities to the use of this pattern in PHP applications.
The main feature of a Front Controller as implemented by the PHP framework is the lazy loading of helpers and controllers, which are created only if used. The Front Controller chooses the Action Controller to run, and then instances only its class for the current request (while in other approaches all the controllers may live in between requests.)
An advantage of this practice is the light weight of the object-oriented infrastructure on the performance of the application. A disadvantage is the complexity of the Front Controller, which may have to manage the Dependency Injection of the various controllers. In general, in the first generation of PHP frameworks the Action Controllers are created with an empty constructor to ease the work of the Front Controller. The second generation (Zend Framework 2 and Symfony 2) promises to support Dependency Injection.
An issue of the Front Controller pattern in PHP is its overhead, inserted as we have said in every request. For this reason, an application may provide multiple end points (multiple Facade .php files), where frequent, more specific requests are directed by the client's browser. For example, population of forms selects via Ajax can skip the Front Controller and refer to a public/ajax.php file. With this trade-off we lose the single entry point, but we bypass the overhead for a large set of requests with very little glue code.
Besides these issues, the Front Controller pattern is the natural evolution of a web application. There are strong pros to the usage of this pattern when the size of the application increases:
- The Front Controller factors out common code from Page Controllers like parsing of the request and creation of a Response object.
- It provides a single entry point for managing every request, so you won't have to change one thousand files if you change the name of header.php which is included by them.
- It may accept plugins or a decoration process to provide new functionalities to all the application, since it intercept every single request.
Examples
The code sample presented here is a simplified version of the Front Controller of Zend Framework 1.x. The real Front Controller has many object collaborators, like a configurable Router and a Dispatcher.
Unfortunately, the implementation of this particular Front Controller is a Singleton, which must be reset in testing environments instead of recreated. The Api provides, Zend_Controller_Front::getInstance() is a large breakage of the Law of Demeter.
This implementation is very versatile however: you can inject plugins which are executed with parameters like the request object at a certain time (before or after a action is chosen or run), or helpers to provide as collaborators to all the Action Controllers.
<?php
/**
* @category Zend
* @package Zend_Controller
* @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Controller_Front
{
// ...private members omitted...
/**
* Constructor
*
* Instantiate using {@link getInstance()}; front controller is a singleton
* object.
*
* Instantiates the plugin broker.
*
* @return void
*/
protected function __construct()
{
$this->_plugins = new Zend_Controller_Plugin_Broker();
}
/**
* Singleton instance
*
* @return Zend_Controller_Front
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Resets all object properties of the singleton instance
*
* Primarily used for testing; could be used to chain front controllers.
*
* Also resets action helper broker, clearing all registered helpers.
*
* @return void
*/
public function resetInstance()
{
$reflection = new ReflectionObject($this);
foreach ($reflection->getProperties() as $property) {
$name = $property->getName();
switch ($name) {
case '_instance':
break;
case '_controllerDir':
case '_invokeParams':
$this->{$name} = array();
break;
case '_plugins':
$this->{$name} = new Zend_Controller_Plugin_Broker();
break;
case '_throwExceptions':
case '_returnResponse':
$this->{$name} = false;
break;
case '_moduleControllerDirectoryName':
$this->{$name} = 'controllers';
break;
default:
$this->{$name} = null;
break;
}
}
Zend_Controller_Action_HelperBroker::resetHelpers();
}
/**
* 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)
{
self::getInstance()
->setControllerDirectory($controllerDirectory)
->dispatch();
}
/**
* 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 string $module Optional argument; module with which to associate directory. If none provided, assumes 'default'
* @return Zend_Controller_Front
* @throws Zend_Controller_Exception if directory not found or readable
*/
public function addControllerDirectory($directory, $module = null)
{
$this->getDispatcher()->addControllerDirectory($directory, $module);
return $this;
}
/**
* 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)
{
if (is_string($request)) {
if (!class_exists($request)) {
require_once 'Zend/Loader.php';
Zend_Loader::loadClass($request);
}
$request = new $request();
}
if (!$request instanceof Zend_Controller_Request_Abstract) {
require_once 'Zend/Controller/Exception.php';
throw new Zend_Controller_Exception('Invalid request class');
}
$this->_request = $request;
return $this;
}
/**
* 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)
{
if (is_string($response)) {
if (!class_exists($response)) {
require_once 'Zend/Loader.php';
Zend_Loader::loadClass($response);
}
$response = new $response();
}
if (!$response instanceof Zend_Controller_Response_Abstract) {
require_once 'Zend/Controller/Exception.php';
throw new Zend_Controller_Exception('Invalid response class');
}
$this->_response = $response;
return $this;
}
/**
* 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)
{
if (!$this->getParam('noErrorHandler') && !$this->_plugins->hasPlugin('Zend_Controller_Plugin_ErrorHandler')) {
// Register with stack index of 100
require_once 'Zend/Controller/Plugin/ErrorHandler.php';
$this->_plugins->registerPlugin(new Zend_Controller_Plugin_ErrorHandler(), 100);
}
if (!$this->getParam('noViewRenderer') && !Zend_Controller_Action_HelperBroker::hasHelper('viewRenderer')) {
require_once 'Zend/Controller/Action/Helper/ViewRenderer.php';
Zend_Controller_Action_HelperBroker::getStack()->offsetSet(-80, new Zend_Controller_Action_Helper_ViewRenderer());
}
/**
* Instantiate default request object (HTTP version) if none provided
*/
if (null !== $request) {
$this->setRequest($request);
} elseif ((null === $request) && (null === ($request = $this->getRequest()))) {
require_once 'Zend/Controller/Request/Http.php';
$request = new Zend_Controller_Request_Http();
$this->setRequest($request);
}
/**
* Set base URL of request object, if available
*/
if (is_callable(array($this->_request, 'setBaseUrl'))) {
if (null !== $this->_baseUrl) {
$this->_request->setBaseUrl($this->_baseUrl);
}
}
/**
* Instantiate default response object (HTTP version) if none provided
*/
if (null !== $response) {
$this->setResponse($response);
} elseif ((null === $this->_response) && (null === ($this->_response = $this->getResponse()))) {
require_once 'Zend/Controller/Response/Http.php';
$response = new Zend_Controller_Response_Http();
$this->setResponse($response);
}
/**
* Register request and response objects with plugin broker
*/
$this->_plugins
->setRequest($this->_request)
->setResponse($this->_response);
/**
* Initialize router
*/
$router = $this->getRouter();
$router->setParams($this->getParams());
/**
* Initialize dispatcher
*/
$dispatcher = $this->getDispatcher();
$dispatcher->setParams($this->getParams())
->setResponse($this->_response);
// Begin dispatch
try {
/**
* Route request to controller/action, if a router is provided
*/
/**
* Notify plugins of router startup
*/
$this->_plugins->routeStartup($this->_request);
try {
$router->route($this->_request);
} catch (Exception $e) {
if ($this->throwExceptions()) {
throw $e;
}
$this->_response->setException($e);
}
/**
* Notify plugins of router completion
*/
$this->_plugins->routeShutdown($this->_request);
/**
* Notify plugins of dispatch loop startup
*/
$this->_plugins->dispatchLoopStartup($this->_request);
/**
* Attempt to dispatch the controller/action. If the $this->_request
* indicates that it needs to be dispatched, move to the next
* action in the request.
*/
do {
$this->_request->setDispatched(true);
/**
* Notify plugins of dispatch startup
*/
$this->_plugins->preDispatch($this->_request);
/**
* Skip requested action if preDispatch() has reset it
*/
if (!$this->_request->isDispatched()) {
continue;
}
/**
* Dispatch request
*/
try {
$dispatcher->dispatch($this->_request, $this->_response);
} catch (Exception $e) {
if ($this->throwExceptions()) {
throw $e;
}
$this->_response->setException($e);
}
/**
* Notify plugins of dispatch completion
*/
$this->_plugins->postDispatch($this->_request);
} while (!$this->_request->isDispatched());
} catch (Exception $e) {
if ($this->throwExceptions()) {
throw $e;
}
$this->_response->setException($e);
}
/**
* Notify plugins of dispatch loop completion
*/
try {
$this->_plugins->dispatchLoopShutdown();
} catch (Exception $e) {
if ($this->throwExceptions()) {
throw $e;
}
$this->_response->setException($e);
}
if ($this->returnResponse()) {
return $this->_response;
}
$this->_response->sendResponse();
}
}
Opinions expressed by DZone contributors are their own.
Comments