eZ Components - MvcTools
Table of Contents
Introduction
The MvcTools component provides an application developer with all the tools to architect his application framework. It is not meant to provide a full framework but merely provides the different parts that can be used. In this tutorial we'll use MvcTools to build HelloMvc, a classic application that greets the visitor in the choosen language. Its sources are available in the SVN at: https://svn.apache.org/repos/asf/incubator/zetacomponents/docs/examples/applications/HelloMvc
TheWire is a twitter like application to share information which is architectured with MvcTools and avalaible in SVN as well, at https://svn.apache.org/repos/asf/incubator/zetacomponents/docs/examples/applications/TheWire/.
Class Overview
The MvcTools component provides classes dealing with the different parts of an MVC framework. It provides functionality for dispatching, routing, views generation, input parsing, output generation and filters. The diagram below shows the code flow through all the different parts.
The next few sections describe the different parts of the diagram. How this all ties together follows in the section .
Request Parsers
The request parser is responsible for taking input from a specific source and creating an abstract ezcMvcRequest object from this input data.
- ezcMvcHttpRequestParser
- Uses HTTP input to create a request object. This is the request parser you would use in most cases, as it's meant for the Web part of applications.
- ezcMvcMailRequestParser
- Uses an e-mail message to create a request object from. The request parser requires an ezcMail object and therefore this request parser is only available through the MvcMailTiein component.
Router
The router analyses the request data in the abstract ezcMvcRequest object and decides which controller should be used to handle the incoming request data. The router uses route objects to matched against request data. Those route objects do the matching themselves, and there are multiple implementations available. Each route is linked to a controller class, an action name and an optional set of extra variables that are either set in the route object instantiation, or defined by URL parameters.
- ezcMvcRouter
- Is an abstract class that should be inherited in the application to define the routes with the help of route objects of the classes ezcMvcRailsRoute and ezcMvcRegexpRoute. Matches are done against the full URI part of the request information.
- ezcMvcRailsRoute
- A route that uses a rails-like URL matcher to match against the URI. The pattern accepts parameters in URL elements starting with : (a colon). An example of a pattern is
/rss/tag/:tagName
where:tagname
denotes a variable URL element with the name tagName. - ezcMvcRegexpRoute
- This route class uses regular expressions to match URLs. Variables are defined by using named sub patterns. An example of a pattern is
@^people/(?P<name>.*)$@
— where(?P<name>.*)
defines the location of a variable URL element with the name name. The ezcMvcRegexpRoute patterns are more complex than ezcMvcRailsRoute patterns, but also more powerful because you can name one URL position different depending on the contents such as in@^people(/((?P<nr>[0-9]+)|(?P<name>.+)))?$@
(the variable nr is returned if a sequence of numbers is matched, otherwise the variable name is returned.
Controller
The controller is created in the dispatcher by using information that is returned from the router (in the form of an ezcMvcRoutingInformation object). It is up to the dispatcher on how the controller is created.
- ezcMvcController
- Every controller should inherit from this abstract class, which implements a constructor that sets the action method and creates object variables for each of the request variables. The abstract class also implements the createResult() method that will be called by the dispatcher to run an action. The implementations of the actions should be done in the inherited class.
View Handlers
Each action returns an abstract object of the class ezcMvcResult. This object contains a set of variables that have to be rendered by a view. A view is selected by the dispatcher and controls the rendering of different sections (zones).
- ezcMvcView
- Is the abstract class that views should inherit from. Every view class should implement the createZones() method to configure the different zone of a view. Each zone can have a different view handler. And each view handler implements ezcMvcViewHandler interface.
- ezcMvcPhpViewHandler
- This view handler uses a plain PHP script to process the result variables. All result variables are available as properties on the $this object.
- ezcMvcJsonViewHandler
- The JSON view handler collects all the variables, and the result from previous zones into an array that is then encoded as JSON.
- ezcMvcTemplateViewHandler
- This view handler uses the Template component for rendering result variables. It is part of the MvcTemplateTiein package. Variables are simply passed as template variables.
Response Writer
The response writers are responsible for outputting the rendered result.
- ezcMvcHttpResponseWriter
- Uses HTTP to output the rendered result. This is what you would normally use for web applications. The response writer will also set the correct headers.
Dispatchers
The dispatcher is responsible for the whole flow of a request. Dispatchers should implement the ezcMvcDispatcher interface. The component comes with only one basic dispatcher at the moment.
- ezcMvcConfigurableDispatcher
- The configurable dispatcher takes as parameter an object of a class that implements the ezcMvcDispatcherConfiguration interface. The dispatcher uses information from this object to decide on any of the above mentioned categories.
- ezcMvcDispatcherConfiguration
- This interface describes the methods for creating the request parser, router, views and response writers. Besides the above mentioned elements it also is responsible for creating the correct request object for fatal errors and the selection and running of filters. Where the ezcMvcConfigurableDispatcher object is responsible for running the code, a class implementing the ezcMvcDispatcherConfiguration interface is responsible for selecting and configuring the application's specifics.
A Simple Application: Hello World
Before we go all the way with a complex application such as TheWire we'll be implementing a very simple application with only a few routes, one controller and two views. From this simple base we will then continue later on with adding more complex elements and end up with TheWire.
Set-up
First of all, we need to create a directory structure. Because we do not want all of our code available directly through the web server will will place the libraries and other related files outside of the document root. We therefore create four directories under our application root: cache, lib, templates and www.
In the top level directory (HelloMvc) we place a config.php file, where we set the include path, add the class repository and configure the Template component. The file is otherwise really simple:
- <?php
- // Set the include path to the eZ Components location, and bootstrap the
- // library. The two lines below assume that you're using eZ Components from
- // SVN -- see the installation guide at http://ezcomponents.org/docs/install.
- ini_set( 'include_path', '/home/derick/dev/ezcomponents/trunk:.' );
- require 'Base/src/ezc_bootstrap.php';
- // Add the class repository containing our application's classes. We store
- // those in the /lib directory and the classes have the "hello" prefix.
- ezcBase::addClassRepository( dirname( __FILE__ ) . '/lib', null, 'hello' );
- // Configure the template system by telling it where to find templates, and
- // where to put the compiled templates.
- $tc = ezcTemplateConfiguration::getInstance();
- $tc->templatePath = dirname( __FILE__ ) . '/templates';
- $tc->compilePath = dirname( __FILE__ ) . '/cache';
- ?>
In the "cache" directory, we create a subdirectory "compiled_templates" and give write access to the apache group:
$ mkdir cache/compiled_templates
# chgrp nogroup cache/compiled_templates
# chmod g+w cache/compiled_templates
We create a file "index.php" in the "www" directory. This file contains the bootstrapping code that gets the application going. Again, the contents of this file are very simple:
- <?php
- // Include the configuration file
- include '../config.php';
- // Instantiate the dispatcher configuration object.
- $config = new helloMvcConfiguration();
- // Send the configuration to the dispatcher, and run it.
- $dispatcher = new ezcMvcConfigurableDispatcher( $config );
- $dispatcher->run();
- ?>
After the above steps, our directory structure now looks like:
HelloMvc
├── cache/
│ └── compiled_templates/
├── lib/
├── templates/
├── www/
│ └── index.php
└── config.php
Dispatcher Configuration
The dispatcher configuration controls the inner workings of the application. We first have to create the autoload file, and place the entry for the helloMvcConfiguration in this autoload file. The autoload.php file goes into the "lib/autoload" directory and for now should contain the following:
- <?php
- return array(
- 'helloMvcConfiguration' => 'config.php',
- ); ?>
The config.php file we create in the "lib" directory, it contains the helloMvcConfiguration class that implements the ezcMvcDispatcherConfiguration interface:
- <?php
- class helloMvcConfiguration implements ezcMvcDispatcherConfiguration {
The next few paragraphs introduce all the methods that this class needs to implement. They control the different aspects, from request parsing to response writing.
We start with the createRequestParser() method, which is required to return a request parser object that will be used to gather information from the environment. We're going to write a web site, so we're going to use the ezcMvcHttpRequestParser class. The method creates a parser object, and then we set the prefix to the directory in which the application is run (as seen through the browser):
function createRequestParser()
{
$parser = new ezcMvcHttpRequestParser;
$parser->prefix = preg_replace( '@/index\.php$@', '', $_SERVER['SCRIPT_NAME'] );
return $parser;
}
After the dispatcher created an ezcMvcRequest object with the request parser, it creates a router object through the createRouter() method. This method accepts the created ezcMvcRequest object so that it could chose a different router depending on information contained in the request object. We don't need that here however, so we just return the user-created router object directly:
function createRouter( ezcMvcRequest $request )
{
return new helloRouter( $request );
}
We'll create the router object itself as first thing after the rest of the dispatcher configuration. We will create two routes, "/" for a general "Hello World" greeting and "/" + name for a personalized greeting. The router and dispatcher will find a controller, execute the action and return a result in the form of an ezcMvcResult object. This object needs to be processed with view handlers. View handlers are selected by returning a specific view class from the createView() method of the dispatcher configuration. For each of the two routes, we create a view. We can do that by using the 'matchedRoute' property of the route information object, which is also passed as argument to the createView() method. Our createView() method looks like:
function createView( ezcMvcRoutingInformation $routeInfo,
ezcMvcRequest $request, ezcMvcResult $result )
{
switch ( $routeInfo->matchedRoute )
{
case '/:name':
return new helloNameView( $request, $result );
default:
return new helloRootView( $request, $result );
}
}
In case the route '/:name' matches, it returns the helloNameView view and otherwise the helloRootView. We'll get back to the implementations of those views later.
After the view has rendered the result, the rendered result needs to be transported back to the client. In order to select such a response writer, the dispatcher calls the createResponseWriter() method. In our case we're only interested in HTTP and therefore we'll just select the ezcMvcHttpResponseWriter as you can see in the implementation of this method:
function createResponseWriter( ezcMvcRoutingInformation $routeInfo,
ezcMvcRequest $request, ezcMvcResult $result,
ezcMvcResponse $response )
{
return new ezcMvcHttpResponseWriter( $response );
}
The last method that we use, is the createFatalRedirectRequest() method. This method is called by the configurable dispatcher when no route could be found by the router, or when the view rendering threw an Exception. The purpose of the createFatalRedirectRequest() method is to reconstruct a new ezcMvcRequest object containing the URL parameters that the router will link to a controller/action handling a fatal request. In our simple example, we'll basically redirect to a "personal greeting" page with as name "FATAL". The fatal redirect is an internal redirect. You need to be aware that if the processing of this fatal redirect requests generate other fatal errors, the code will loop. The configurable dispatcher has an internal redirect limit of 25. If this limit is reached, an ezcMvcInfiniteLoopException is thrown. Our createFatalRedirectRequest() method looks like:
function createFatalRedirectRequest( ezcMvcRequest $request,
ezcMvcResult $result,
Exception $response )
{
echo $response->getMessage();
$req = clone $request;
$req->uri = '/FATAL';
return $req;
}
In this method, during development, it is probably wise to output the error message contained in the exception with something like we do with the echo statement. You would not want that in a production environment of course as you don't want your users to see your error messages like this raw.
We're cloning the original request here to keep all the original request parameters (user agent, request time, etc). With this last method, we conclude the helloMvcConfiguration class. You can find the whole fine in SVN at https://svn.apache.org/repos/asf/incubator/zetacomponents/docs/examples/applications/HelloMvc/lib/config.php
There are four other methods defined in the interface. Those methods deal with running filters on request, result and response data. We will be using the runResultFilters() method to automatically add an "installRoot" variable to the variables that are available in the views. It's trivial to do so as the implementation of the runResultFilters() method shows:
function runResultFilters( ezcMvcRoutingInformation $routeInfo, ezcMvcRequest $request, ezcMvcResult $result )
{
$result->variables['installRoot'] = preg_replace( '@/index\.php$@', '', $_SERVER['SCRIPT_NAME'] );
}
We are not using the other three methods in this example, but they still have to be present because they're part of the interface:
function runPreRoutingFilters( ezcMvcRequest $request )
{
}
function runRequestFilters( ezcMvcRoutingInformation $routeInfo, ezcMvcRequest $request )
{
}
function runResponseFilters( ezcMvcRoutingInformation $routeInfo, ezcMvcRequest $request, ezcMvcResult $result, ezcMvcResponse $response )
{
}
Creating The Router
The first thing to implement is the router object. We're going to place this in the "lib/" directory with the name "router.php". First of all, we have to add this entry to the autoload file at "/lib/autoload/autoload.php". Add the following line below the 'helloMvcConfiguration' line:
'helloRouter' => 'router.php',
The router class should inherit from the ezcMvcRouter class, and re-implement the createRoutes() method. This method is expected to return an array with objects that implement the ezcMvcRoute interface. The MvcTools component comes with two implementations: ezcMvcRailsRoute and ezcMvcRegexpRoute. We'll be using the ezcMvcRailsRoute class as it is slightly easier to use. The router class' implementation is then really simple:
- <?php
- class helloRouter extends ezcMvcRouter
- {
- public function createRoutes()
- {
- return array(
- new ezcMvcRailsRoute( '/downloadTest', 'helloTestController', 'download' ),
- new ezcMvcRailsRoute( '/:name', 'helloController', 'greetPersonally' ),
- new ezcMvcRailsRoute( '/', 'helloController', 'greet' ),
- );
- }
- }
- ?>
Each route defines a pattern ('/' or '/:name') and links that to a controller (helloController) and an action ('greet' or 'greetPersonally').
The controller is created by the dispatcher, which assumes the controller class will be loaded through the autoload mechanism. If you do not want your controllers to have to use the autoload mechanism, you can inherit from the configurable dispatcher, and override the createController() method. See the class documentation for ezcMvcConfigurableDispatcher for more information about what this method's signature is.
Creating the Controller
The controller implements the real logic of the application. In our case that is of course not a whole lot as we'll only echo a greeting. We start again by adding the controller to the autoload.php file. Add the following line below the 'helloRouter' line:
'helloController' => 'controllers/hello.php',
Then we proceed creating the controller class in the 'lib/controllers' directory. We will inherit from the ezcMvcController class that implements calling action methods depending on the $this->action property that is set by the dispatcher. The action method is matched with the method name by using a very simple algorithm:
The action name is split up by '_'.
Every element is run through ucfirst to uppercase the first character.
The method name is assembled by using "do" and then appending every element (without the '_').
Examples:
"list" turns into "doList()".
"greeting" turns into "doGreeting()".
"greet_personally" turns into "doGreetPersonally()".
"greetPersonally" turns into "doGreetPersonally()".
In our case, that means we'll have to implement the doGreet() and doGreetPersonally() methods in our inherited class. Every action method is required to return an object of the class ezcMvcResult or the class ezcMvcInternalRedirect. We will only use the ezcMvcResult class in our first example.
Our doGreet() method will select a random language to use as greeting, we'll do the same for the doGreetPersonally() method but there we'll also set the person's name as variable on the ezcMvcResult object. Because we're sharing functionality between two methods, we create another method to select a random language's greeting:
- <?php
- class helloController extends ezcMvcController
- {
- private function selectGreeting()
- {
- $greetings = array( 'Hello', 'Hei', 'こんにちわ', 'доброе утро' );
- return $greetings[mt_rand( 0, count( $greetings ) - 1 )]; }
The doGreet() method uses this method to select a greeting, and adds this to the result as the 'greeting' variable. It then returns the result object:
public function doGreet()
{
$ret = new ezcMvcResult;
$ret->variables['greeting'] = $this->selectGreeting();
return $ret;
}
The doGreetPersonally() method doesn't do a whole lot more. Compared to the doGreet() method above it adds another array key, "person", and set's it value to the $this->name variable. This "name" variable name comes from directly from the router where this part of the URL was defined with ":name". This means that the URL "/Derick" would cause the $this->name variable to be set to "Derick". The method below just passes this on as the "person" variable to the view handlers that will render the results from this action method.
public function doGreetPersonally()
{
$ret = new ezcMvcResult;
$ret->variables['greeting'] = $this->selectGreeting();
$ret->variables['person'] = $this->name;
return $ret;
}
The whole controller file can be found in SVN as: https://svn.apache.org/repos/asf/incubator/zetacomponents/docs/examples/applications/HelloMvc/lib/controllers/hello.php
It is also possible to return different results than the above normal type. You can do so by setting the $ret->status property to an instance of either ezcMvcExternalRedirect or ezcMvcResultUnauthorized.
Creating the Views
Now we've the abstract result object we can render this result with the two view that we'll be using: helloRootView and helloNameView. We place the two view files in the "lib/views/" directory and add the following two lines to the autoload.php file:
'helloRootView' => 'views/root.php',
'helloNameView' => 'views/name.php',
Before we create the view classes, the concept of zones should be explained. Zones are a way for arranging different parts of a layout. Take for example the following layout:
In this small example there are three zones:
The "menu" zone, where we'll put in a link to the home page.
The "content" zones, where we'll put the greeting.
The "pagelayout" zone, which encapsulates the others -- it provides the main layout and HTML headers, stylesheets, etc.
Each view that you define can include multiple zones, each with their own associated name and view handler. Zones are processed in order, and the result of each processed zone is added as a variable for use for subsequent zones. Because this becomes clearer with an example, we now show the helloRootView class that we put into the 'lib/views/' directory as root.php:
- <?php
- class helloRootView extends ezcMvcView
- {
- function createZones( $layout )
- {
- $zones = array();
- $zones[] = new ezcMvcTemplateViewHandler( 'menu', 'menu.ezt' );
- $zones[] = new ezcMvcPhpViewHandler( 'content', '../templates/generic_greeting.php' );
- $zones[] = new ezcMvcTemplateViewHandler( 'page_layout', 'layout.ezt' );
- return $zones;
- }
- }
- ?>
Here we define the three zones in order. First, the view would process the "menu.ezt" template with the template view handler. The result of this will be assigned to the "menu" variable. This "menu" variable would show up just like any other variable in a result object, which means it can be used in both the "content" and "page_layout" views. In our example, the "content" view will not make use of this, but the "page_layout" view will to put the content of the "menu" view (and also the "content" view) in the correct spot on the page. This mechanism prevents you from having to include the menu, content (and any other template) from the "page_layout" template and prevents you from having to send all the required variables along to the included templates. The zone mechanism also allows you to use different view handlers for different parts of the layout. In our case we use a template for the "menu" and "page_layout" zones, but the plain PHP for the "content" zone.
The helloNameView class is put in the 'lib/views/' directory as name.php:
- <?php
- class helloNameView extends ezcMvcView
- {
- function createZones( $layout )
- {
- $zones = array();
- $zones[] = new ezcMvcTemplateViewHandler( 'menu', 'menu.ezt' );
- $zones[] = new ezcMvcPhpViewHandler( 'content', '../templates/personal_greeting.php' );
- $zones[] = new ezcMvcTemplateViewHandler( 'page_layout', 'layout.ezt' );
- return $zones;
- }
- }
- ?>
The templates themselves we place in the "templates/" directory. We keep them as simple as possible. First the "menu.ezt" template:
{use $installRoot}
<div id="menu">
<a href="{$installRoot}/">Home Page</a>
</div>
Secondly the "generic_greeting.php" PHP script:
- <div id="greeting">
- <?php echo $this->greeting; ?>
- </div>
- <?php
- ?>
We'll also create the "personal_greeting.php" PHP script, that our other view (helloNameView) uses:
- <div id="greeting">
- <?php echo $this->greeting, " ", htmlspecialchars( $this->person ); ?>
- </div>
- <?php
- ?>
And lastly the "layout.ezt" template:
{use $menu, $content}
<html>
<head><title>Hello World</title></head>
<body>
{raw $menu}
<br/>
{raw $content}
</body>
</html>
Please note that we're using the "{raw}" construct to include the already rendered zones. If we would not have done that, all HTML tags in there would be escaped again. This is a feature of the Template component. For the same reason, it's safe to just use "{$person}" in a template, but if you use a PHP style template like we've done here for "generic_greeting.php" and "personal_greeting.php" you need to think of placing htmlspecialchars() around variables that come from the input.
Wrapping it Up
To make the application work properly in Apache, we need to tell it to send all requests to the index.php script. We do that by using mod_rewrite through a .htaccess file. We place this file in the "www/" directory, and fill it with the following text:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
The full directory listing is now:
HelloMvc
├── cache/
│ └── compiled_templates/
├── lib/
│ ├── autoload/
│ │ └── autoload.php
│ ├── controllers/
│ │ └── hello.php
│ ├── views/
│ │ ├── name.php
│ │ └── root.php
│ ├── config.php
│ └── router.php
├── templates/
│ ├── generic_greeting.php
│ ├── layout.ezt
│ ├── menu.ezt
│ └── personal_greeting.php
├── www/
│ ├── .htaccess
│ └── index.php
└── config.php
Security
Overriding the X-Powered-By header
By default, the MvcTools component will add an X-Powered-By HTTP header containing the string "Zeta Components MvcTools" or "eZ Components MvcTools". This can be considered a security issue. If you want to modify this header you can add the following code in your response filter:
$response->generator = "Whatever you want here";