blob: 32f49c04475115501da57b25b00050d7cd0d51ed [file] [log] [blame]
eZ Components - MvcTools
~~~~~~~~~~~~~~~~~~~~~~~~
.. contents:: 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:
http://svn.ez.no/svn/ezcomponents/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
http://svn.ez.no/svn/ezcomponents/docs/examples/applications/TheWire/.
__ http://twitter.com
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.
.. image:: img/dispatcher-flow.png
:alt: Request flow
The next few sections describe the different parts of the diagram. How this all
ties together follows in the section _`Dispatchers`.
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:
.. include:: HelloMvc/config.php
:literal:
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:
.. include:: HelloMvc/www/index.php
:literal:
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
http://svn.ez.no/svn/ezcomponents/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:
.. include:: HelloMvc/lib/router.php
:literal:
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()".
__ http://php.net/ucfirst
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
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:
.. image:: img/zones.png
:alt: Layout zones
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:
.. include:: HelloMvc/lib/views/root.php
:literal:
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:
.. include:: HelloMvc/lib/views/name.php
:literal:
The templates themselves we place in the "templates/" directory. We keep them
as simple as possible. First the "menu.ezt" template:
.. include:: HelloMvc/templates/menu.ezt
:literal:
Secondly the "generic_greeting.php" PHP script:
.. include:: HelloMvc/templates/generic_greeting.php
:literal:
We'll also create the "personal_greeting.php" PHP script, that our other view
(helloNameView) uses:
.. include:: HelloMvc/templates/personal_greeting.php
:literal:
And lastly the "layout.ezt" template:
.. include:: HelloMvc/templates/layout.ezt
:literal:
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:
.. include:: HelloMvc/www/.htaccess
:literal:
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
..
Local Variables:
mode: rst
fill-column: 79
End:
vim: et syn=rst tw=79 nocin