Varnish Edge Site Includes (ESI) - Zend Framework plugin controller

Auteur: Jeroen van Dijk

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:

<?php 
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:

<?php
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.

<?php
protected function _replaceEsiIncludes($data)
{
    
$hash hash("crc32"$data);
    
$esi = array();
    if (
preg_match_all(self::ESI_INCLUDE_REGEX$data$esiPREG_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:

<?php
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=([0-9]+)~"$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.

<?php
Glitch_Controller_Plugin_EsiParser
::setCache(<Zend_Cache_Core object>);
$front Zend_Controller_Front::getInstance();
$front->registerPlugin(new Glitch_Controller_Plugin_EsiParser());
?>

Download the source file: Source
View the presentation on SlideShare

Useful links:
Zend Framework controller plugins
Zend Framework application resources


Share |
Jeroen van DijkOver Jeroen van Dijk

Jeroen van Dijk is mede-oprichter en senior developer bij 4worx, de open source technology tak van Enrise. Voor complexe vraagstukken bedenkt hij technisch-creatieve oplossingen die uitblinken in schoonheid en eenvoud.




Reageren



Reacties (2)

  • Matthew Weier O'Phinney (19 juli 2010 17:00)

    This isn't quite the same as ESI, however. ESI is supposed to solve the issue of personalization of pages -- where the content is primarily static, but certain elements of the page derive from personalized content. Varnish handles the edge-side includes for you, caching the content individually, and ensuring that the appropriate personalization is then injected for the final page returned to the user.

    This helps offload functionality -- the main page may be cached by Varnish already, so that it only needs to fetch and cache the dynamic elements that need to be included via ESI. The result is less load on your server.

    The method you outline here appears to be trying to mimic ESI... but at the application level. I guess I'm failing to see how this will bring down server load, as you're still requiring a full application instance for the main page...

  • Jeroen (19 juli 2010 19:10)

    @matthew It's not supposed to replace Varnish, or bring down server load. It's merely a replacement for Varnish while doing the development. Lots of times people working on the project may not have enough knowledge to work with Varnish. This plugin controller can come in handy!

Contactgegevens

Enrise B.V.
Grote Koppel 7-B
3813AA Amersfoort
welcome@enrise.com
Tel +31 [0]88 555 33 00
Fax +31 [0]88 555 33 01
Routebeschrijving

Got l33t?

Enrise heeft vacatures en stageplekken voor getalenteerde nerds.
Check onze jobs

Basecamp log-in

Klik 4worx.basecamphq.com

Nog geen inloggegevens voor uw project ontvangen? Neem contact op met uw accountmanager.

Wij werken met

Magento eCommerce platform

Wordpress

ExpressionEngine

Apache Software Foundation