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 the MvcTools and related components to build TheWire —
a twitter like application to share information. The source code of TheWire is
available in SVN as well, at
http://svn.ez.no/svn/ezcomponents/docs/examples/applications/TheWire/.
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.
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
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 )
{
$req = clone $request;
$req->uri = '/FATAL';
return $req;
}
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
http://svn.ez.no/svn/ezcomponents/docs/examples/applications/HelloMvc/lib/config.php
There are three 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 two methods in this example, but they still have to
be present because they're part of the interface:
function runRequestFilters( ezcMvcRoutingInformation $routeInfo, ezcMvcRequest $request )
{
}
function runResponseFilters( ezcMvcRoutingInformation $routeInfo, ezcMvcRequest $request, ezcMvcResult $result, ezcMvcResponse $response )
{
}
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( '/: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.
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:
http://svn.ez.no/svn/ezcomponents/docs/examples/applications/HelloMvc/lib/controllers/hello.php
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>
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>
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.
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
RewriteRule HelloMvc$ /$0/ [L,R]
RewriteRule !.*\.(css|ico)$ index.php
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