Pragmatism in the real world

Zend Framework Views and the Front Controller

Following on from my last post, I’ve now played with integrating Zend_View into the system. Whilst I was playing, I discovered that Nucleuz has put up a tutorial on wiki.cc. I quite like his approach, but needing to call $this->display(); in every controller’s action strikes me as a pain. Another problem is that you can forward from one action to another, and so the view needs to follow with you.

The approach in this entry is my first attempt (that works!) to satisfy my requirements of making handling the view as transparent as possible.

Initially, I thought that using the Front Controller’s plugin functionality would be useful, except that the plugins have no access to the actual actions. Therefore, I went with Nucleuz idea of subclassing the Zend_Controller_Action.

First of all, this is the directory structure I am using:

htdocs/lib/
/lib/Akrabat
/lib/Akrabat/Action.php
/lib/Akrabat/Router.php
/lib/Zend/
/lib/Zend.php

/wwwroot
/controllers
/controllers/IndexController.php

/views
/views/index/
/views/index/index.tpl.php
/views/latestNews.tpl.php
/views/site.tpl.php

/wwwroot/.htaccess
/wwwroot/index.php

This makes more sense if you read the last post :)

Now, to make the View available to all the controller actions that may be called, we create it in the index.php and store it in the Zend::registry. Then we can pick it up in each controller action.

The interesting bit of index.php now looks like this:

$router = new Akrabat_Router();

$dispatcher = new Zend_Controller_Dispatcher();
$dispatcher->setControllerDirectory('./controllers');

$frontContoller = Zend_Controller_Front::getInstance();
$frontContoller->setControllerDirectory('./controllers');
$frontContoller->setRouter($router);
$frontContoller->setDispatcher($dispatcher);

$view = new Zend_View();
$view->setScriptPath('./views');
Zend::register('view', $view);
unset($view); // don't need it here any more

$frontContoller->dispatch();

$view = Zend::registry('view');
echo $view->render('site.tpl.php');

The first bit I’ve already covered, so I’ll only deal with the new bits:

$view = new Zend_View();
$view->setScriptPath('./views');
Zend::register('view', $view);
unset($view); // don't need it here any more

This is fairly simple code; create the Zend_View, setup the default directory to find the view files and then assign to the registry. We don’t technically need to unset it, but it makes it clear that the $view variable is no longer in use.

We will also need to render the view after all the controllers have done their stuff:

$view = Zend::registry('view');
echo $view->render('site.tpl.php');

Again, very simple code; pick up the view from the registry and then render the “master” template which I’m calling site.tpl.php.

All that’s left now is to pick up data for the view from the controller actions…

Usage

Before that though, let’s consider the usage of all this…

The main intention is to ensure that the using the system is as seamless as possible and avoid having to think about how it all fits togher. Thus in the controller we want to write:

$this->title = 'Welcome to my website';

and the title would be available in the view template automatically.

We’d also like one “master” template for our site and then a separate “content” template for each controller action. This’ll enable reuse of templates and controllers.

The Extended Controller

To tie the view to the controller, I’ve extended Zend_Controller_Action as Akrabat_Action in Akrabat/Action.php.

< ?php

/**
* Akrabat_Action allows us to tie up the view to the controller
*/

/** Zend_Controller_Action */
require_once 'Zend/Controller/Action.php';

abstract class Akrabat_Action extends Zend_Controller_Action
{
private $assignedVars;
protected $actionTemplate = null;

/**
* @var Zend_View
*/
protected $view;

function __construct()
{
$assignedVars = new StdClass();
$this->view = Zend::registry('view');
}

/**
* Assuming that we aren't using persistant controllers, the destructor
* gets called after every action. Obviously the first time this is called
* is for the first action which we'll consider to be the "master" action
* and so assign it to "content"
*
*/
function __destruct()
{
$action = $this->_action->getActionName();
$defaultActionTemplate = $this->_action->getControllerName() . DIRECTORY_SEPARATOR . $action . '.tpl.php';

// assign the variables created by the controller to an array named after the action
$this->view->assign($action, $this->assignedVars);

// create a template for the action in $templates
// consider the first action to be the main action and so assign it's template to
// $templates['content']
if(is_object($this->view->Templates))
{
$templates = $this->view->Templates;
}
else
{
$templates = new StdClass();
}

if(!isset($templates->content))
{
$templates->content = $this->actionTemplate ? $this->actionTemplate : $defaultActionTemplate;

// assign all the "content" action's variables directly into the View's scope. This is
// useful for variables that are required in the master template, like pageTitle.
foreach($this->assignedVars as $name => $value)
{
$this->view->assign((string)$name, $value);
}

}
// assign a template for this action into $templates->[action_name]. Note that by default
// this is [controller_name]/[action_name].tpl.php
$templates->$action = $this->actionTemplate ? $this->actionTemplate : $defaultActionTemplate;
$this->view->assign('Templates', $templates);
}

// magic functions used so that any "invented" properties created in
// the command functions get assigned to $assigned_vars and hence passed
// to the view
public function __get($name)
{
if(isset($this->assignedVars->$name))
{
return $this->assignedVars->$name;
}
return false;
}

public function __set($name, $value)
{
$this->assignedVars->$name = $value;
}
}
?>

There’s a lot of code there, so I’ll break it down a bit. The dispatcher creates a new instance of each controller and then calls the action function and then the controller goes out of scope and is hence destructed. We take advantage of this by picking up the view from the registry in the contstuctor. Then, in the destructor, we collect whatever variables have been assigned in the action and put them into the view.

Picking up the variables requires the use of PHP 5’s new magic functions __set() and __get(). These functions let us intercept any variables that are assigned to in the class functions. i.e. the code

$this->title = 'Welcome to my website';

results in the function __set(); being called. We use this to store the variable into a private property called $assignedVars; Then when we get to the destructor, we assign all variables in $assignedVars to the view.

The other thing we need to handle is the templates that we want to use. To do this we use a special variable in the view called $Templates and for each action, we fill in a property for that action. The default value assigned to an action’s template is views/{controller_name}/{action_name}.tpl. Thus the action index() will have an associated template of $view->Templates->index = ‘views/index/index.tpl.php’. Of course, there’s a protected variable in the action called $actionTemplate, so any given action can override this default.

The Templates

All that’s required now is to pick up the information we have stored in the templates. This is done from site.tpl.php:

< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>< ?php  if($this->pageTitle) {echo $this->escape($this->pageTitle) . ' -'; } ?>  My Test Site</title>
</meta></head>
<body>
<h1>< ?php  echo $this->pageTitle ?   $this->escape($this->pageTitle) : ''; ?></h1>

< ?php $this->render($this->Templates->content); ?>

</body>
</html>

First thing to notice that any variable assigned in the first controller action is available in the global scope of the template. e.g. we can get at the page title using:

<h1><?php  echo $this->pageTitle; ?></h1>

We also need to render our action’s template:

<?php $this->render($this->Templates->content); ?>

This will then render views/index/index.tpl.php for the default controller action and would render /blog/view.tpl.php if the controller action was BlogController::view();

Lastly, we need to get a variables in our action template such as views/index/lastestNews.tpl.php:

<?php echo $this->latestNews->body; /*note: no escaping as we trust this data from the action */ ?>
<ul>
<?php foreach($this->latestNews->news as $newsItem) : ?>
<li><?php echo $this->escape($newsItem); ?></li>
<?php endforeach; ?>
</ul>

All, in all, it’s easy really :)

Example Code

I’ve uploaded some example code: ZF View Test v1 to show it all in action:

I’ve only tested on windows and assume that the zf_view_test directory is in the root of your webserver. Try these two urls to show it all in action:
http://localhost/zf_view_test/wwwroot/
http://localhost/zf_view_test/wwwroot/index/latestNews

The action latestNews() is used twice: once directly and once as a forwarded action from index(). In both cases we reuse the template views/index/latestNews.tpl which is exactly what we want.

Obviously, if you find any bugs, let me know :) It’ll be interesting to compare this approach to the “official” way when we get some documentation for Zend_Controller.

8 thoughts on “Zend Framework Views and the Front Controller

  1. You probably want to have $this->assignedVars instead of $assignedVars in the contructor of Akrabat_Action :)

  2. I tried to install the above and got an error in Akrabat/Router.php as there is no require_once Zend/Controller/Dispatcher/Action.php in the version of Zend Framework I am using. Version 0.6.0. Any ideas?

  3. Hi Simon,

    I suspect that this code has "bit-rotted" considerably now that version 0.6 is out. Funnily enough, we're currently in the process of doing something similar at work, so I'll try and blog about it soon.

    Regards,

    Rob…

  4. Btw, is really needed a new Router class ? Not better to have just a new Route Class ? I mean with overwrite of Router class we can't then run a proper route->assemble function to generate url…

  5. Christian,

    This post was written when the Zend Framework was at version 0.1…

    Akrabat_Router was written to solve the problem of getting it to work on IIS without mod_rewrite and is certainly not the way to do it nowadays :)

    Regards,

    Rob…

  6. Ok, thanks.

    I used your idea (zend view directly into index.php, aso) anyway, solved some of the headaches i had last days with finding proper architecture…

Comments are closed.