Varnish Edge Site Includes – Zend Framework plugin controller

Development on websites when the product will run on a Varnish’ed’ production environment can be a pain in the ass. The xml tag that can be used to define Edge Side Includes can’t be parsed by a standard browser. While developing you often look at a half rendered website implementation. Testing the site will be very hard. Setting up a Varnish installation for multiple users in a testing environment seems also somewhat cumbersome.

Because of this, while using Zend Framework during a large project we implemented a plugin controller that mimics the behavior of the internal ESI parser of Varnish.

It’s an easy task to create the parser to replace the ESI tags. An ESI tag is defined using the following xml tag:

 <esi:include src="/path/to/url" /> 

Which we can detect with the following regular expression:

 const ESI_INCLUDE_REGEX = '~<esi:include src="(.+?)"[ ]*/>~s'; 

The detection of the ESI tags can be run on dispatch loop shutdown in the Zend Framework dispatch structure. Basically the implementation would look like this:

 class Glitch_Controller_Plugin_EsiParser extend Zend_Controller_Plugin_Abstract { public function dispatchLoopShutdown() { // read the current body generated in the response object // replace the esi tags with parsed content // replace the body with the new body including the parsed esi tags } } 

This method needs to detect the ESI tags, replace them in the body content and return the renewed body content to the client. It needs to do this in recursive way, because ESI snippets could contain another ESI snippet which is not detected on the first run. The following method does the recursive detection of ESI snippets.

 protected function _replaceEsiIncludes($data) { $hash = hash('crc32', $data); $esi = array(); if (preg_match_all('~~s', $data, $esi, PREG_SET_ORDER) > 0) { foreach($esi as $snippet) { $content = $this->_replaceEsiInclude($snippet[1]); $data = str_replace($snippet[0], $content, $data); } } if ($hash != hash('crc32', $data)) { return $this->_replaceEsiIncludes($data); } return $data; } 

The physical replacement is done using the following method:

 protected function _replaceEsiInclude($url) { $uri = 'http://'.$_SERVER['HTTP_HOST'] . $url; $key = $this->_getCacheId($uri); if ($this->_cacheEnabled()) { // detect if url is cached $data = self::$_cache->load($key); if (false !== $data) { return $data; } } $fp = fopen($uri, 'r', false, $this->_httpContext); if (false !== $fp) { $data = stream_get_contents($fp); if ($this->_cacheEnabled()) { $meta = stream_get_meta_data($fp); foreach ($meta['wrapper_data'] as $header) { $match = array(); if (preg_match('~Cache-Control: max-age=(\d+)~', $header, $match)) { if (false !== $data) { // cache url with the respected max-age setting self::$_cache->save($data, $key, array(), intval($match[1])); break; } } } } fclose($fp); return $data; } return null; } 

The _replaceInclude method does the replacement using stream contexts to fetch the metadata that comes with the call. Because of this no extra libraries are needed to mimic the Varnish behavior. This makes the PHP code even suitable to be run on Windows or other development environments that don’t have a Varnish installation to the test the code on.

The reference to the cache load and store are calls to the generic load and save methods defined in the Zend_Cache module. Your own selected backend can be set statically before the plugin controller executes any code.

View the presentation on SlideShare

Useful links:
Zend Framework controller plugins
Zend Framework application resources

Leave a Reply

Your email address will not be published. Required fields are marked *