WebIssues/MVC Pattern
Contents
Introduction to MVC
The architecture of many modern web applications is based on a common MVC pattern consisting of three layers:
- The Model represents the information on which the application operates its business logic.
- The View renders the model into a web page suitable for interaction with the user.
- The Controller responds to user actions and invokes changes on the model or view as appropriate.
Each layer can be further divided. The model usually consists of a database abstraction layer and a data access and manipulation layer. The view consists of a layout common to all pages, templates of individual pages and logic which glues these two together. The controller contains a front controller, which does the work common to all controllers, and actions which contain only the controller code specific to one page.
An example of a framework implementing such model is symfony, which I took as a reference because it has very good documentation and clean code. There are however some important differences between the architecture of symfony-based applications and the design of the WebIssues Server and it's custom framework.
Multiple Entry Points
Many applications, including symfony, use URL rewriting so that all request are handled by a single entry point and the action to invoke is determined by the URL, for example:
www.example.com/article/view/123
URL rewriting also allows to use user-friendly URLs and make the dynamic application resemble a static website, for example:
www.example.com/2009/07/introduction-to-mvc.html
Such approach is not needed in case of an application like WebIssues, which doesn't need to be indexed by search engines and doesn't require user-friendly URLs. So the WebIssues framework will use traditional URLs, for example:
www.example.com/client/viewfolder.php?id=10&page=4
The advantage is that we don't need to set routing rules and configure mod_rewrite. It also makes WebIssues easier to deploy on Windows with IIS where URL rewriting is usually not available.
Page/View Coupling
In many traditional MVC applications, actions and views are not tightly coupled; the same view can be rendered by many different actions, and on the other hand, one action can by rendered using different views depending on the result (for example success/failure) or context.
Though this is a powerful mechanism, it can also be difficult to manage and can make the code less clear. In WebIssues, each page object implements a single action and has a single associated view template. Page logic and template are stored in two files with the same location and names, but different extension, for example:
client/index.php client/index.tpl.php
This makes it easy to find a template related to a page and vice versa.
If an action performed by a page should result in another page being displayed, it should send an appropriate redirect header. That will also make browser's history behave correctly and prevent resubmitting the same form multiple times.
Application
The WebIssues Server doesn't have a single entry point, but it still requires a single object which implements logic common to all pages or a group of pages. This object is called the application. It serves as the front controller which executes the appropriate page action and outputs the rendered view. The application class can be extended to implement special behavior for a group of pages, for example to assign a common decorator to them or to implement security for the administration area.
The application also creates and holds references to other objects providing information about the current context of execution, such as the request, response, session, logger, etc. The reference to the global application object is available anywhere in the code; in addition references to frequently used components such as the request and response objects are available as members of all components and views.
Components
Usually to render a single page more that one template is needed. A page template can include other templates, for example common controls or blocks which can appear on multiple pages. It can also be decorated with another template, for example page content can be decorated with the layout template common to all pages.
The common parts and decorators also consist of a logic layer and a view layer, in a similar was as pages. The WebIssues framework introduces a concept of a component, which is an object implementing logic associated with a template. A page is simply a special case of a component and it is actually its subclass. The template associated with a page can use other components and their templates can subsequently use their own components.
The example object tree of an application can look as follows:
Application | +- Page | +- PageView | +- DecoratorComponent | | | +- DecoratorComponentView | +- PartComponent | +- PartComponentView | +- SubPartComponent | +- SubPartComponentView
A decorator acts just like a regular component, except that it's content is not inserted into the containing view, but rather the content of the containing view is inserted into an appropriate placeholder of the decorator. This allows for example to wrap page content in a layout or to wrap a block in a frame. So the final content of the rendered page will look as follows:
+- DecoratorComponentTemplate ------------------------------------------------------+ | | | +- PageTemplate ------------------------------+ | | | | | | | +- PartComponentTemplate ----------+ | | | | | | | | | | | +- SubPartComponentTemplate -+ | | | | | | | | | | | | | | | | | | | | | | +----------------------------+ | | | | | | | | | | | +----------------------------------+ | | | | | | | | | | | +---------------------------------------------+ | | | +-----------------------------------------------------------------------------------+
Sample Page
The implementation of a page looks like this:
<?php require_once( 'system/bootstrap.inc.php' ); class Index extends System_Web_Page { protected function __construct() { parent::__construct(); } protected function execute() { $this->response->setTitle( 'Hello World' ); $this->data[ 'user' ] = 'MiMec'; } } bootstrap( 'Common_Application', 'Index' );
The bootstrap.inc.php
file is the main file of the WebIssues framework which initializes the environment and provides a function for creating and running the application.
All other files are included automatically using the PHP __autoload mechanism. For that to work, all classes must be declared in files whose path (relative to the root WebIssues directory) matches the class name, except that underscores are replaced with slashes, all letters are converted to lowercase and the .inc.php
extension is appended. For example a class named System_Web_Page
is declared in a file named system/web/page.inc.php
. This convention also helps prevent name conflicts.
The bootstap()
function creates the object of the given application class and optionally passes the second parameter to its constructor. Then it calls the run()
method of the application. In the example above, Common_Application
is a common application class used by all regular web pages. It derives System_Web_Application
which implements the front controller of the MVC pattern and overrides its preparePage()
method, setting default parameters such as the main layout decorator and default page title. The second parameter passed to bootstrap()
is the name of the page which the application will in turn create and execute.
The page class must implement at least the execute()
method which should contain the logic of the page action. It may read data from the request, interact with the models, set parameters of the response and define data items which will be accessible to the view template.
A view template associated with this page may look like this:
<?php if ( !defined( 'WI_VERSION' ) ) die( -1 ); ?> <h1>Welcome</h1> <p>Hello, <?php echo $user ?>!</p>
The data items are accessible directly as local variables. The view object is also accessible using the $this
variable and it provides access to the request, response, etc.
The template should use the alternative syntax and should generally contain as few PHP code as possible, as all logic should be handled by the page class. For compatibility short tags such as <?=$user?>
should not be used.
The check for WI_VERSION
prevents executing the template file directly by typing its name in the URL. It's only defined if the bootstap.inc.php
file was already included by another script. Generally all PHP files should either include bootstap.inc.php
or have this check in the first line.
There are a few other useful constants available:
-
WI_SCRIPT_PATH
is the full path of the entry script -
WI_ROOT_PATH
is the path of the directory where WebIssues is installed -
WI_SCRIPT_URL
is the full URL of the entry script -
WI_BASE_URL
is the URL of WebIssues root directory
Always use WI_ROOT_PATH
for loading or including files and use WI_BASE_URL
for creating absolute links within the application.
Non-MVC Applications
Not all WebIssues applications have to use the MVC pattern. For example, the server handler and the cron job will use a custom mechanism of executing actions without using components, pages and views. So the main part of the WebIssues framework is divided into two modules: core and web. The core module implements a basic context of execution, request and response objects, session handling, etc., while the web module implements the MVC pattern.
If you don't want to use the MVC pattern, simply define an application deriving System_Core_Application
and implement the execute()
method. Then call the bootstrap()
function with the name of the application class and an optional parameter which can have any meaning.