Deprecated Behaviour

The inane, sometimes insane, ramblings from the mind of Brenton Alker.

Updated Extension to Zend Framework's ViewRenderer to Add Layouts

A few months back, when I was first starting with the Zend Framework, I posted my extension to the ViewRenderer helper. This is the helper that the Controller uses to work with the View. My extension was in order to implement site-wide and controller-wide layouts, allowing a consistent look and feel in an application.

I have been fairly happily using this extension since then, more recently though, I got back to fixing some problems with it. One problem I noted (and updated the previous post to reflect), was it’s double calling of the action. After further investigation, it turns out it wasn’t calling it twice, but had an image with an empty src tag in the layout, causing the browser to request the page twice.

However, It still gave me an excuse to have another look at it, and fix up some other issues. So here I am posting the revised code. If you are using the old one, it isn’t backwards compatible, but there isn’t much to change and I think it’s a much nicer implementation. It uses a stack to render the layouts in order, and is therefore much more extensible to multi-level layouts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123


class Custom_Controller_Action_Helper_ViewRenderer extends Zend_Controller_Action_Helper_ViewRenderer
{
    /**
     * Defaults stack of layout scripts, listed with the innermost
     * layout first.
     *
     * @var array
     */
    protected $_defaultStack = array(
        ':controller/layout',
        'layout',
    );
    
    /**
     * The stack of layout scripts, listed with the innermost first.
     * 
     * @var array
     */
    protected $_stack = null;
    
    /**
     * If the layouts should be rendered or not
     * 
     * @var boolean
     */
    protected $_renderLayout = true;

    /**
     * Constructor
     *
     * Set the viewSuffix to "tpl.php" unless a viewSuffix option is
     * provided in the $options parameter. And set the stack to the default
     *
     * @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);
        
        $this->_stack = $this->_defaultStack;
    }
    
    
    /**
     * Add a layout to the stack
     * 
     * Add a layout to be rendered inside the current stack 
     * of layouts
     * 
     * @see Zend_Controller_Action_Helper_ViewRenderer::_translateSpec()
     * 
     * @param string  The filename, possibly using variables as described for _translateSpec(...)
     * 
     * @return    boolean The same as array_unshift 
     */
    public function addLayout($script)
    {
        return array_unshift($this->_stack, $script);
    }
    
    
    /**
     * Clears the layout stack, removing the default stack and any
     * layouts that have been added.
     * 
     * @return array  The previous layout stack
     */
    public function clearLayout()
    {
        $stack = $this->_stack;
        $this->_stack = array();
        return $stack;
    }

    
    /**
     * Render the action script, then each of the layouts in the stack in
     * order, assigning the content of each to the content variable in
     * the next. Append the result to the Response's body.
     *
     * @param string $script
     * @param string $name        The response segment to append to
     */
    public function renderScript($script, $name = null)
    {
        $this->view->baseUrl = $this->_request->getBaseUrl();
        if (null === $name) {
            $name = $this->getResponseSegment();
        }
        
        // get the content of the action
        $content = $this->view->render($script);
        $this->view->content = $content;
        
        if ($this->_renderLayout) {
            foreach ($this->_stack as $layout) {
                $layoutScript = $this->_translateSpec($layout.'.:suffix');
                $content = $this->view->render($layoutScript);
                $this->view->content = $content;
          }
        }
        
        $this->getResponse()->appendBody($content, $name);
    }

    /**
     * Enable, or disable the rendering of layouts
     * 
     * @param boolean Whether the layouts should be rendered or not
     */
    public function setLayoutEnabled($enabled = true)
    {
        $this->_renderLayout = $enabled;
    }
}

The implementation in the view script is very similar to the previous incarnation, except the view is no longer responsible for rendering the children itself, this is done beforehand, and is passed to the layout in the “content” variable. Leaving the layout to look something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title><?php echo (isset($this->title)) ? $this->escape($this->title) : 'Untitled'; ?></title>
</head>
    <body>
        <?php echo $this->content; ?>
    </body>
</html>

And, unlike the previous version, all levels of layout work the same way. So the controller layout would look like this:

1
2
3
4
5
6


<h1>Default Controller</h1>
<h2><?php echo $this->escape($this->title); ?></h2>
<?php echo $this->content; ?>

To use these layouts, they can be added to the stack, like so (from the controller):

1
2
3
4
5
6


$this->_helper->viewRenderer->clearLayout(); // clear the existing layouts
$this->_helper->viewRenderer->addLayout(':controller/layout');
$this->_helper->viewRenderer->addLayout('layout');

But this is the default anyway.

Until the Zend framework standardizes the layout situation, with Zend_View_Enhanced or Zend_Layout, or any combination thereof, this method will feature in my applications.