Pragmatism in the real world

Extending ViewRenderer to Support Layout Templates

Contrary to what my tutorial says, I’m one of those people that doesn’t want to have to render my common header and footer templates in every single view script file. I prefer a standard site-wide layout script file that will display the content of the action script files within it.

The Zend_Controller_Action_Helper_ViewRenderer action helper is a great bit of code that automates rendering a view template based on which action has been called. This is very useful, but renders the action template, not my layout template. To solve this, I am experimenting with extending the Zend_Controller_Action_Helper_ViewRenderer and overriding it so that it know about my layout template. I also prefer to use the view suffix “tpl.php” for my view scripts, so I’ve made my class automatically set my preferred view suffix.

On to the code…

The master site template is called site.tpl.php and lives in the views/scripts/ directory. A simplistic breakdown of it looks like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">



< ?php echo $this->escape($this->pageTitle);?>
<link rel="stylesheet" href="< ?php echo $this->baseUrl; ?>/css/site.css"
type="text/css" media="screen" />

< ?php echo $this->render($this->actionScript); ?>



$this->actionScript is the script associated with the current action which is automatically determined by Zend_Controller_Action_Helper_ViewRenderer, so in the case of the index action with the index controller, the action script is views/scripts/index/index.tpl.php.

As Zend_Controller_Action_Helper_ViewRenderer doesn’t know about site.tpl.php and we want to render that instead, we extend like this:

<?php
class Controller_Action_Helper_ViewRenderer
extends Zend_Controller_Action_Helper_ViewRenderer
{
/**
* Name of layout script to render. Defaults to 'site.tpl.php'.
*
* @var string
*/
protected $_layoutScript = 'site.tpl.php';

/**
* Constructor
*
* Set the viewSuffix to "tpl.php" unless a viewSuffix option is
* provided in the $options parameter.
*
* @param Zend_View_Interface $view
* @param array $options
* @return void
*/
public function __construct(Zend_View_Interface $view = null,
array $options = array())
{
if (!isset($options['viewSuffix'])) {
$options['viewSuffix'] = 'tpl.php';
}
parent::__construct($view, $options);
}

/**
* Set the layout script to be rendered.
*
* @param string $script
*/
public function setLayoutScript($script)
{
$this->_layoutScript = $script;
}

/**
* Retreive the name of the layout script to be rendered.
*
* @return string
*/
public function getLayoutScript()
{
return $this->_layoutScript;
}

/**
* Render the action script and assign the the view for use
* in the layout script. Render the layout script and append
* to the Response's body.
*
* @param string $script
* @param string $name
*/
public function renderScript($script, $name = null)
{
$this->view->baseUrl = $this->_request->getBaseUrl();
if (null === $name) {
$name = $this->getResponseSegment();
}

// assign action script name to view.
$this->view->actionScript = $script;

// render layout script and append to Response's body
$layoutScript = $this->getLayoutScript();
$layoutContent = $this->view->render($layoutScript);
$this->getResponse()->appendBody($layoutContent, $name);

$this->setNoRender();
}
}

All we need to do now is modify our bootstrap so that the new ViewRenderer is used rather than the default one. I do this in index.php before the first instantiation of the front controller:


<?php
// Use our ViewRenderer action helper
$viewRenderer = new Controller_Action_Helper_ViewRenderer();
Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);

That’s all there is to it. My action scripts now just contain the HTML specific to the action and my layout template is automatically rendered with the action’s HTML in the right place!

Obviously this is my first cut on this and so is fairly rough at the edges. Thoughts and improvements always welcome!

32 thoughts on “Extending ViewRenderer to Support Layout Templates

  1. I am in specific cotroler which need's to use specific template.. how to call
    ->setLayoutScript('mynew.tpl.php') ?

  2. Hi Vaidas,

    From within your controller, you can do:

    < ?php $viewRenderer = $this->getHelper('ViewRenderer');
    $viewRenderer->setLayoutScript('newlayout.tpl.php');

    Regards,

    Rob…

  3. Funny enough, it seems that we want to blog about the same topics all the time.

    Good thing I checked your blog before starting my own article ;).

  4. Cool stuff, Rob. Coming from a Java background, I was looking for something similar to the tiling that you can do in frameworks such as Struts. What you've posted should allow me to do what I want to do, so thanks!

    Regards,
    William

  5. I like this!

    However, in a conventional modular layout, whilst it seems to work for the default module, for any other it hits: Fatal error: Uncaught exception 'Zend_View_Exception' with message 'script 'site.phtml' not found in path' in /home/zf/src/ZendFramework-20070604-5096/library/Zend/View/Abstract.php:853 Stack trace: #0

    Not sure how I should deal with that. As a bodge, symlinking to the site.phtml from the module's scripts dir fixes it.

    Any idea how I could properly fix the script path from the right place for all cases?

    Btw. I used .phtml only to be consistent with the manual. I hope ZF makes up it's mind on recommended file extensions (I've commented on it in the draft coding standard).

  6. Hello Rob,
    I was hoping that I could get some info about your menu view helper.

    I imagine that it outputs html to create your menu. What is the benefit of using a view helper as opposed to doing something like render(menu.phtml)? Would you mind sharing your code for that helper?

    Thanks,
    Mark

  7. Hi,

    Just to follow up on my June 6th post, I needed to addScriptPath() early in the bootstrap to ensure it would be at the back of the script path stack.

    It's also no sweat to turn off your extended functionality in the viewRenderer with a simple method like say setNoLayout() and call it from an action method in the controller with say:
    Zend_Controller_Action_HelperBroker::getExistingHelper('viewRenderer')->setNoLayout();

    Useful when an action doesn't want it's output wrapped up in HTML, like maybe batch work or image serving.

  8. Hi Mark,

    $this->menu() is hypothetical at the moment as the one I actually have is written in Smarty. The Smarty one takes an array and turns it into a nested unsigned list. CSS then does the magic to make it look like a menu.

    Regards,

    Rob…

  9. Hi Rob,

    great guide thx!
    quick question… is it possible to have the layout file/s in a dir located on the main level (ex. /templates)?
    the idea is to keep the script views in the standard ZF directories and have a separate top level /template dir including only templating layouts
    do you think is possbile?

    thanks in advance
    Giulio

  10. Giulio,

    try doing something like this:
    $layoutContent = $this->view->render('layouts/'.$layoutScript);

    take a look at the values in $this->view->getScriptPaths() to make any further adjustments accordingly…

    Clayton

  11. Guilio,

    I think Clayton has beat me too it!

    You could try something like:

    $layoutContent = $this->view->render('../../templates/'.$layoutScript);

    Regards,

    Rob…

  12. thanks for the replies
    i solved in another way: inside the class Controller_Action_Helper_ViewRenderer i added $this->view->addScriptPath('./templates/'); inside the function renderScript()
    seems to work fine

  13. Very nice indeed but what if you want to use some AJAX?

    For example, I have a registration page with an ajax request to see if a username is still free
    my call would be to /register/checkusername

    Lets say I just want to output a json with "free" or "already registered"

    do you need to create another bootstrap for this or is there a better solution?

    thanks!

  14. I've been doing some further reading and found this to work but it just doesn't seem right..
    $viewRenderer = $this->getHelper(‘ViewRenderer’);
    $viewRenderer->setLayoutScript(‘empty.tpl.php’);
    $this->getResponse()->setHeader('Content-Type', 'text/xml');
    $this->getResponse()->setBody("render();

    is there a better solution?

  15. I don't want to spam your blog but something went wrong with the code:

    $this->getResponse()->setBody('some xml …');
    $this->render();

  16. By implementing your approch I get 2 resulting outputs ; the first one – as expected is the general template corresponding to the site.tpl.php, but right after that I get a second one corresponding to the invoqued action-script (let's say index.phtml).

    The cause is that somehow both viewRenderers live in the helpBroker; the extended one we create but also the default one which is dispatched right after the first one.

    The question now is, how do I kill the default one without harming my own renderer? *Obviously setting a custom view doesn't prevent the instanciation of the default one, as opposite to what Brady says here http://blog.astrumfutura.com/archives/290-Having-a-bad-ViewRenderer-day-in-your-ZF-app.html*

  17. My mistake guys,

    The problem was that my own class had a different name – it was …_MyViewRenderer and not …_ViewRenderer. That's way there were two different helpers in the broker.

    Sorry

  18. How can I use your approach together with smarty?
    I used to $this->view->subtemplate = ('file.tmp') and than inside the template I called it with {incluce file=$subtemplate}

  19. Daniel,

    Sorry, not sure. Would have to look into it. At the moment, we are on the fence as to whether to use Smarty in our ZF apps or not.

    Regards,

    Rob…

  20. i have it working for now – but only via the full absolute path including in every action, that cannot be the right thing.

    I finally ended up with a ZF-conform layout structure for modules – having a "modulename"/views/scripts/"controller"/action file-structure…. –
    I would rather have a kind structure like a views directory where I have the same strucute like the web, where i can style all the necessary templates and call them with a simple makeSubtemplate…..or whatever.

    Not even thinking about things like a global menu or some modules that only need to be accessed by a few sites

  21. I'm new to all this and confused…

    Where do I save the class file: Controller_Action_Helper_ViewRenderer
    ??

    Thanks
    Simon

  22. Simon,

    in Controller/Action/Helper/ViewRenderer.php.

    Usually, you would prefix with a "vendor" name, such as Akra. In which case the full class is Akra_Controller_Action_Helper_ViewRenderer which lives in lib/Akra/Controller/Action/Helper/ViewRenderer.php.

    Regards,

    Rob…

  23. what if i have some viewHelpers that need javaScript. How can I incle that in "site.tpl.php"?.
    Regards..

  24. This is great, and exactly what I was after, but I've un into a small problem.

    The Helper is throwing up the following error:

    Controller_Action_Helper_ViewRenderer::$_request in /var/www/proficcymod/lib/LLGC/Controller/Action/Helper/ViewRenderer.php on line 64

    PHP Fatal error: Call to a member function getBaseUrl() on a non-object in /var/www/proficcymod/lib/LLGC/Controller/Action/Helper/ViewRenderer.php on line 64

  25. Regarding M's question (15-17), why not just call, from the controller,

    $this->viewRenderer->setLayoutScript(false);

    and then in your implementation of renderScript, do this first:

    if(!$this->getLayoutScript) {
    parent::renderScript($script, $name);
    }

    It seems that would be the easiest way of disabling the layout script functionality completely.

  26. Hey, I just started working with ZF.. So far everything is okay… Except, I really don't like the fact that if I work with modules My views have to be in "modules/moduleName/views/scripts/:controller/:action" .
    I was wondering if it was actually possible to make it this way:
    "application/views/moduleName/:controller/:action"

    It would mean a lot if you could help,

    Angel

  27. @Dan: You can replace the line with the following:

    $this->view->baseUrl = $this->getRequest()->getBaseUrl();

Comments are closed.