Zend_Layout vs. template inheritance

2009 October 31

Zend_Layout has always felt a little odd to me. It solves the problem of having redundant code in view scripts in applications with many pages using the same template. The solution seems simple: capture the output of the view script, assign it to a member of the master view script, then render the master view script. But does it really need to register a controller plugin, an action helper and a view helper in order to accomplish that? It feels over-engineered to me. This also makes it very closely coupled to the MVC stack, making it practically unusable by itself.

I am currently migrating an old PHP application to ZF. I am doing it bit by bit, and I started by converting the templates to Zend_View. Shortly after, it became clear that I also need to use a two-step view since it’d be a pain to change the design later if I don’t DRY up the view scripts. The manual claims that Zend_Layout can be used by itself. It’s technically true, but in practice, it’s nothing more than a thin wrapper to Zend_View outside of the MVC stack because of it’s reliance on the Zend_Controller_Front components. I could just as well use a second instance of Zend_View to render the master template.

Template Inheritance

What I really needed was template inheritance. I considered using another library instead of Zend_View that implements template inheritance like Twig or Dwoo, but then I’d have problems later on when I migrate the rest of the app to Zend MVC. There’s this solution by zomg, but that one also uses a plugin and a helper.

My Implementation

My solution consists of two classes, a view helper and a view filter. The view filter is there just for convenience (it makes the call to render() optional). The classes are here. Drop Zend_View_Helper_Extend into APPLICATION_PATH/views/scripts and Zend_View_Filter_Extend into APPLICATION_PATH/views/filters. Sample project will come later (maybe).

Basic usage:

/* application/views/scripts/foo/bar.phtml */
<?php $layout = $this->extend('master.phtml') // relative to the script path, complete with extension ?>
<?php $layout->section('content') ?>
lorem ipsum dolor sit amet
<?php $layout->end() ?>
<?php echo $layout->render() ?>

/* application/views/scripts/master.phtml */
the quick brown fox <?php echo $this->content ?>.

Assuming you have defined a FooController with a method barAction(), the result would be the quick brown fox lorem ipsum dolor sit amet..

Any text outside the sections would be rendered normally. Since that is undesirable most of the time, a section is automatically opened after calling extend() with a non-null parameter. By default, it opens the content section, although you could change that by specifying a second parameter to extend(). The end() calls are optional, since sections cannot be nested anyway. The open section is automatically closed when section() or render() is called. I recommend omitting calls to end() except when defining default sections (see below). As already mentioned, render() is optional, thanks to the companion filter. So in simple use cases, the “slave” view script can be reduced to this:

(Aside: the filter feature of Zend_View is practically undocumented. I don’t think it was even mentioned in the reference manual.)

/* application/views/scripts/foo/bar.phtml */
<?php $this->extend('master.phtml') ?>
lorem ipsum dolor sit amet

The master.phtml script could extend() another master, as long as its master isn’t a slave (or a slave of a slave) of master.phtml. That’s one thing it has over Zend_Layout, although I honestly don’t see myself needing more than one master.

Defining default sections

A master view can also define the default content of a section in case it wasn’t declared by the slave.

// application/views/scripts/foo/bar.phtml
<?php $this->extend('master.phtml') ?>

// application/views/scripts/master.phtml
<?php $this->extend()->define('foo') ?>
'foo' is not defined.
<?php $this->extend()->end() ?>

<?php echo $this->foo ?>

In this example, the foo section wasn’t declared in the slave, so the output becomes 'foo' is not defined.. define() should be called before echoing the view variable, so always group the define() calls and keep them at the top.

A slave can “retrieve” the default content using super() inside the section() blocks.

// application/views/scripts/foo/bar.phtml
<?php $this->extend('master.phtml') ?>
the quick brown <?php $this->extend()->super() ?> jumps over the lazy <?php $this->extend()->super() ?>.

// application/views/scripts/master.phtml
<?php $this->extend()->define('content') ?>
*fox*
<?php $this->extend()->end() ?>

The output of the above is the quick brown *fox* jumps over the lazy *fox*.. Take note that super() is not echoed. “Retrieve” is in quotes because it’s not really what it does. The master view isn’t evaluated until the end of the slave, so it is impossible for the slave to get the default section content from the master. This function is really screwy when using nested master views. There’s also the problem of possibly having multiple default sections when using nested masters . I haven’t really given this much thought since, as I said, I don’t really see myself using more than one master view.

There you have it, a two-step view that is more decoupled and flexible in far fewer LOC than Zend_Layout. A master layout with sidebars might look like this:

// application/views/scripts/layout.phtml
<? $layout = $this->extend() ?>
<? $layout->define('title') ?>
[meh]
<? $layout->define('sidebar') ?>
There's no extra content.
<? $layout->end() ?>

<?= $this->doctype() ?>
<html>
<head>
<title><?= $this->title ?></title> <!-- yeah, yeah, I should've used headTitle() here -->
<?= $this->headMeta() ?>
<?= $this->headLink() ?>
<?= $this->headStyle() ?>
<?= $this->headScript() ?>
</head>

<body>
<div id="content">
    <div id="main-content">
        <?= $this->content ?>
    </div>

    <div id="extra-content">
        <?= $this->sidebar ?>
    </div>
</div>
</body>
</html>

// application/views/scripts/index/index.phtml
<? $layout = $this->extend('layout.phtml') ?>

<h1>Lorem Ipsum</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc quis lacinia ligula. Mauris a arcu ac massa malesuada luctus non ac orci. Sed auctor nibh vel augue adipiscing in tincidunt eros semper. Nulla et vestibulum leo. Praesent at molestie arcu. Fusce scelerisque mollis augue vitae tempor. Etiam mi turpis, sollicitudin eget ornare vitae, eleifend et magna. Donec sed velit sem. Curabitur dapibus porttitor quam at vehicula. Integer et nisi est. Nullam ullamcorper ligula id ligula aliquet vitae adipiscing massa tincidunt. Vestibulum ultricies tincidunt ornare. Aliquam ac ante eget augue sollicitudin cursus.</p>

<? $layout->section('title') ?>
<? $layout->super() ?> Layout demo

<? $layout->section('extra') ?>
<h4>More content</h4>
<p>The quick brown fox jumps over the lazy dog.</p>

OT: I’m planning on starting a blog on a new server, so the updates here would become even more sporadic. Apologies to the visitors who left comments some time ago.

2 Responses leave one →
  1. 2009 November 25
    brian permalink

    Hi, thanks so much for posting this… I’ve been searching for a solution exactly like the one you have provided! Unfortunately I’m new to Zend so I’m getting lost in the configuration of this plug-in. In particular, I’m getting the error: Plugin by name ‘Extend’ was not found in the registry; used paths: Zend_View_Helper_:

    Do you think you can upload a sample project so I can understand the complete implementation? Thanks!

    • 2009 November 26
      monzee permalink

      Hi,
      You first need to create 2 files named Extend.php in the paths application/views/helpers/ and application/views/filters/. Go to this gist, paste the class Zend_View_Helper_Extend into the file application/views/helpers/Extend.php, then paste Zend_View_Filter_Extend into application/views/filters/Extend.php. Those prefixes are registered by default so you don’t have to do anything special for them to be discoverable by the view.

      I added a demo project here. You can download it via svn export http://mz-project.googlecode.com/svn/tags/others/extend-demo.

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS