PHP, Zend Framework and Other Crazy Stuff
Archive for June 5, 2007
Having a bad ViewRenderer day in your ZF app?
Jun 5th
Over the last week a lot of the activity on the Zend Framework mailing lists has revolved around the introduction in 1.0.0 RC1 of the ViewRenderer action helper. As of RC1 this helper has been enabled by default. Lot’s of queries have been raised about how to disable, modify it, and generally how current applications can be made to work with the ViewRenderer.
The ViewRenderer “action helper” is the class Zend_Controller_Action_Helper_ViewRenderer. It’s primary purpose is to facilitate the automated rendering of View scripts (templates) based on the generally accepted Zend Framework conventions. It’s these conventions which will cause a lot of people grief, since the previous reliance on programmers defining the template to render has likely led to inconsistent template names. The helper itself replaces much of the View integration methods contained in Zend_Controller_Action like initView(), render(). This is a good thing, since it decouples the automated View code from the Controller class. Decoupling is hugely important in allowing classes to be modified or replaced easily and efficiently.
The problems most people are having boil down to a few simple ones:
1. ViewRenderer is enabled by default
2. Inconsistent naming of template files
3. Inconsistent location of template files
4. Disagreements with the ZF conventions
5. Reusing template files across Controller actions
The first can be remedied by disabling the ViewRenderer completely. This is easily done by passing the following parameter to the Zend_Controller_Front class:
[geshi lang=php]Zend_Controller_Front::getInstance()->setParam(‘noViewRenderer’, true);[/geshi]
This should be sufficient in many cases, allowing you to pretend the ViewRenderer does not exist. The problem here is that ViewRenderer is not something evil – it’s something good. Using it reduces the code you need to type for all actions. So a more attractive step is adding support for the ViewRenderer to your code. This is where problems 2-4 arise.
The first problem is inconsistent names for templates. I can’t help you much here except advise you use a convention to name templates. Using a convention would allow ViewRenderer to find them easily. Using ad-hoc names without structure makes any level of automation immensely difficult.
The Zend Framework has gradually being documenting a set of conventions for how you should organise your application. The most famous is likely the Conventional Modular Directory Structure which addresses the filesystem. The ViewRenderer action helper introduces another for template naming. It assumes that templates are related on a 1:1 basis with Controller actions and uses a naming convention of the form:
{controllerName}/{actionName}.phtml
For example, the template for IndexController::indexAction would be located at:
index/index.phtml
The full path would therefore be: /src/default/views/scripts/index/index.phtml
From the booing in the audience I gather some of you may disagree
. This is easily remedied by setting your own custom format for template names. In fact you can change a few things in the ViewRenderer by customising your own instance of it! Imagine you did not like splitting templates into separate directories, and kept everything in the form:
index_index.tpl.php
under /src/views/scripts (no “default” Module subdirectory). Then you can do some customisation using the following snippet:
[geshi lang=php]require_once ‘Zend/Controller/Action/Helper/ViewRenderer.php’;
$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer;
$viewRenderer->setViewSuffix(‘tpl.php’);
$viewRenderer->setViewScriptPathSpec(‘:controller_:action.:suffix’);
$viewRenderer->setViewBasePathSpec(APPLICATION_PATH . ‘/views’);
Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);[/geshi]
The above code explicitly creates the ViewRenderer instance, and applies some new settings. You could put it in your bootstrap file. If you want to put it after the Front Controller has been instantiated you’ll need to replace creating a new instance with:
[geshi lang=php]$viewRenderer = Zend_Controller_Action_HelperBroker::getExistingHelper(‘viewRenderer’);[/geshi]
This demonstrates how a decoupled helper makes customised View conventions much easier to implement than you might suspect. Sure it looks more complex, but it’s simpler than replacing the whole class or performaing heavy subclassing.
There is one small issue here. You can influence the BasePath set on a View, but not the individual sub-directories of the BasePath like /scripts, /filters, /helpers. Luckily, ViewRenderer does not override existing paths set on a View, it just adds to the list – so yes, you can customise to this level also by setting a custom View object for the ViewRenderer to chew on using your preferred path settings:
Let’s say we didn’t use a /scripts subdirectory for Views. And we wanted to use a custom Smarty driven View object also.
[geshi lang=php]require_once ‘Zend/Controller/Action/Helper/ViewRenderer.php’;
require_once ‘Zps/View/Smarty.php’;
$smartyView = new Zps_View_Smarty;
$smartyView->setScriptPath(APPLICATION_PATH . ‘/views’);
$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer;
$viewRenderer->setView($smartyView);
$viewRenderer->setViewSuffix(‘.tpl’);
$viewRenderer->setViewScriptPathSpec(‘:controller_:action.:suffix’);
Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);[/geshi]
The above code now allows the ViewRenderer to search two locations for templates. The first is the custom /views. The second is the ViewRenderer set default of /views/scripts. Setting a custom View object prevents the default instantiation of a stock Zend_View instance.
The final problem folk have met is how to selectively reuse templates across actions. In writing a LoginController class recently, I needed to use the same View (a login form template with optional error message) across two actions: indexAction and processAction. The indexAction method would map 1:1 to login_index.phtml. How would I get the processAction method to use the same template, and not the ViewRenderer automated selection of login_process.phtml?
There are actually two methods. Firstly you can render into the Response object manually using:
[geshi lang=php]$this->_helper->viewRenderer->setNoRender();
$this->view->loginError = true;
$this->getResponse()->setBody(
$this->view->render(‘login_index.phtml’)
);
return;[/geshi]
Above I generally get the Response object from Zend_Controller_Front in my bootstrap and manually echo it. There’s no coupling if there’s no automation
. So before 1.0.0 RC1 I avoided all the integration methods until finalised (lucky for me!).
Secondly, you can rely on the earlier View integration method Zend_Controller_Action::render() using:
[geshi lang=php]$this->view->loginError = true;
$this->render(‘index’); // tell ViewRenderer (if enabled!) to render same as indexAction()
return;[/geshi]
Viva la ViewRenderer!
