Simple, automated cache busting JavaScript and CSS in Symfony with eZ Platform and eZ Publish 5.x
By: Peter Keung | April 26, 2017 | eZ Publish development tips
In eZ Publish 5.x and eZ Platform, we use Assetic to manage a website's JavaScript and CSS. By default, if you make a change to your JS or CSS files, the resulting file name ends up the same. This has some undesirable cache implications that vary depending on whether you're using a reverse proxy or CDN, how you've set up browser caching rules, and more. Here's a simple way to version the file names, while still being able to cache the files as much as possible.
To achieve a balance between URLs that can be cached for a long time and having visitors get your JS and CSS updates immediately, we can generate some unique identifier as a URL/GET parameter for the Assetic-compiled CSS and JS files. Symfony can automatically append a version string to the file names, but you have to manually update that in your code. Instead, let's append a timestamp:
/cms/css/c5dee5d.css?1492618627
While we won't have to manually update this parameter, we can't just generate this timestamp directly in our Twig templates; otherwise, each page will end up with a different timestamp, creating a large number of unnecessary URL variations. If there are cached pages that reference a previous timestamp URL, that old JS/CSS version that goes along with those pages is likely still cached. If it's not, since the file name is the same, it loads the new JS/CSS version instead of a broken link.
We just need to figure out how to automatically update the timestamp and when. We can register a new service in services.yml to be run whenever we clear the CMS cache:
# Custom functionality whenever the cache is cleared: currently stores a timestamp for versioning assets mugo.custom_cache_clearer: class: Mugo\BaseBundle\Cache\CustomClearer tags: - { name: kernel.cache_clearer }
Then, in our custom service Mugo/BaseBundle/Cache/CustomClearer.php, we can store the timestamp in the ezsite_data table (which is a simple key-value database table that is native to eZ Publish). Note that for convenience, the \eZSiteData::create function relies on code from a pull request.
<?php namespace Mugo\BaseBundle\Cache; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; /* * Whenever the cache is cleared, update a timestamp value for asset versioning */ class CustomClearer implements CacheClearerInterface { public function clear( $cacheDir ) { // Depends on this pull request: https://github.com/ezsystems/ezpublish-legacy/pull/1273 $assetTimestamp = \eZSiteData::create( 'asset_timestamp', time() ); $assetTimestamp->store(); } }
With that in place, we just need a Twig function to access that timestamp value. To be concise, the code below shows the key PHP function; see the Twig documentation for more information on how to create a Twig function.
/* * Get the asset timestamp for versioning of CSS/JS, based on a timestamp value updated on every cache clear */ public function assetTimestamp() { $assetTimestampObject = \eZSiteData::fetchByName( 'asset_timestamp' ); if( $assetTimestampObject ) { return $assetTimestampObject->attribute( 'value' ); } else { return 'v1'; } }
... then append the timestamp to all JS/CSS files in our templates:
<link href="{{ asset_url }}?{{ asset_timestamp() }}" rel="stylesheet" type="text/css">
There are various alternatives to this approach, which include renaming the files rather than just modifying a URL/GET parameter. This has its own pros (for example, some CDNs won't create new cache variations for GET parameters) and cons (such as dealing with file cleanup).
If you use Grunt, you can use it to increment version numbers. Also, starting in Symfony 3.1, you can define a "custom version strategy" which means you can have a single function that will automatically modify the output of {{ asset_url }} so you don't have to append ?{{ asset_timestamp() }} to each asset path in your templates. (The latest version of eZ Platform officially supports Symfony 2.8, but there are plans for Symfony 3 compatibility.)