I’ve been a fan of Zend Framework for some time now and I’ve recently become quite a fan of Varnish. As you all know, Varnish is an extremely powerful reverse caching proxy (although the Varnish people prefer the term HTTP accelerator).
Varnish supports a subset of the ESI language and I’ve written a custom Zend Framework view helper that implements the ESI basics.
What is ESI?
Before I talk about the view helper itself, I guess it’s important to explain what ESI does and in what context it is used.
ESI stands for Edge Side Includes and is similar to the Server Side Includes we know from the good old days. In Essence ESI renders a page where the ESI tag is put. The tag is merely a placeholder. The difference with SSI is that the tag is not rendered by your browser, but by the caching proxy server (in my case Varnish).
Why ESI?
The most important use case of ESI tags is the ability to make Varnish cache different blocks each with their own time to live. For high traffic sites this can be useful because caching proxy servers usually only cache full pages. If the data of some content blocks changes more frequently than the other, you’re stuck. ESI can solve that.
Content blocks
I’ve included an image that represents the common layout for content blocks. You basically have your header, footer, menu and main page.
In plain old HTML it could look like this:
<table> <tr> <td colspan="2">Header</td> </tr> <tr> <td>Menu</td> <td>Main page</td> </tr> <tr> <td colspan="2">Footer</td> </tr> </table>
As I explained in the previous paragraph, a standard page caching strategy will not take the different data refresh rate of each content block into account. If you set your TTL to low you will be able to display changes quickly, but the effect of the caching will be poor and server load will increase.
If you set your TTL to high, you server load will be just fine, but you’ll suffer from outdated content.
So what you might want is a high TTL on your header and footer, a regular TTL on your menu and a low TTL on your main content page. The reason for that is because your main content page could contain news items that are frequently updated.
Another trick you can use is to purge cache items explicitely. I’ve written a blog post that describes this. Instead of waiting for the page to expire, you force it by purging the item from the cache. This could be quite powerful when combining it with ESI.
Using the ESI tag
The basic ESI tag looks like this:
<esi:include src="http://myurl/" />
Just replace the src attribute and you’re good to go. If we want to convert our previous HTML layout into an ESI aware layout, you could use the following bit of HTML:
<table> <tr> <td colspan="2"><esi:include src="http://myurl/header.php" /></td> </tr> <tr> <td><esi:include src="http://myurl/menu.php" /></td> <td><esi:include src="http://myurl/main.php" /></td> </tr> <tr> <td colspan="2"><esi:include src="http://myurl/footer.php" /></td> </tr> </table>
The view helper
What my view helper does is render ESI tags based on a Zend Framework controller name and action name. It’s very similar to the Zend Framework URL helper. You invoke it in a view or layout file and use the following syntax:
<?php echo $this->esi('index','header'); ?>
The returned string will look like this:
<esi:include src="http://myurl/index/header" />
In the background we just use the Zend Framework router to assemble the ESI URL. The code snippet below shows you how to do that:
<?php $frontController = Zend_Controller_Front::getInstance(); $router = $frontController->getRouter(); echo $router->assemble(array('controller'=>$controller,'action'=>$action)); ?>
Proxy detection
You can imagine that this view helper only works when your webserver is actually behind a proxy server. If not, your brower won’t render the tags and your site will become useless.
To avoid this, the view helper contains some proxy detection logic. We can assume that the proxy server sends an HTTP header to your webserver. When using Varnish, the HTTP_X_VARNISH header is used. If this header is set, we’re behind a proxy and can use ESI, if not, we have to switch to plan B.
Normally Varnish can only render ESI when the URL containing ESI tags is mentioned explicitely. To avoid this lack of flexibility, we can send an HTTP header to the proxy that says which pages use ESI and which pages don’t. I use the esi-enabled: 1 header to do that.
But because we can’t be rely on the fact that Varnish is used or that these HTTP headers are explicitly set, I’ve made sure the view helper supports custom detection headers. These headers are passed to the view helper as the third and fourth argument, but they are optional.
Plan B
Now what is Plan B” I mentioned in the previous paragraph? What if we aren’t behind a proxy that supports ESI? Well … in Zend Framework it’s simple: we just dispatch the controller action and return the rendered HTML”’.
This snippet of PHP code shows you how to use the Zend Framework dispatcher from any context within your project.
<?php $request = new Zend_Controller_Request_Simple($action, $controller); $response = new Zend_Controller_Response_Http(); $frontController = Zend_Controller_Front::getInstance(); $dispatcher = $frontController->getDispatcher(); $dispatcher->dispatch($request, $response); echo $response->getBody(); ?>
The code
The code below is the full view helper, but I you might want to check out the version I put on Github in order to have the latest version. This view helper only works on Zend Framework 1.x, I’m also working on a port for Zend Framework 2.x. The 2.x port will be on my Github account as well.
<?php class My_View_Helper_Esi extends Zend_View_Helper_Abstract { protected $_requestHeader = null; protected $_responseHeader = null; protected $_frontController = null; public function esi($controller = 'index', $action='index', $requestHeader = 'X_VARNISH', $responseHeader = 'esi-enabled: 1') { $this->_requestHeader = $requestHeader; $this->_responseHeader = $responseHeader; $this->_frontController = Zend_Controller_Front::getInstance(); if($this->_getRequestHeader()){ $this->_getResponseHeader(); return "<esi:include src=\"{$this->_buildUrl($controller, $action)}\"/>"; } else { return $this->_dispatch($controller, $action); } } protected function _getRequestHeader() { if($this->_requestHeader === null || $this->_requestHeader === false){ return false; } $request = $this->_frontController->getRequest(); if($request->getHeader($this->_requestHeader) === false){ return false; } else { return true; } } protected function _getResponseHeader() { if($this->_responseHeader !== null && $this->_responseHeader !== false){ header($this->_responseHeader); } } protected function _dispatch($controller,$action) { $request = new Zend_Controller_Request_Simple($action, $controller); $response = new Zend_Controller_Response_Http(); $dispatcher = $this->_frontController->getDispatcher(); $dispatcher->dispatch($request, $response); return $response->getBody(); } protected function _buildUrl($controller,$action) { $router = $this->_frontController->getRouter(); return $router->assemble(array('controller'=>$controller ,'action'=>$action)); } } ?>
[...] recently developed a custom Zend Framework view helper and I needed to find a way to get pieces of rendered HTML out of an action [...]
Hey,
Thanks for this post. It’s very helpful.
G