Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

 

What’s the cache? This SilverStripe module will painlessly boost your site’s performance.

One of the techniques that makes a good developer great is the application of intelligent and level of caching. This module gives you that technique to speed up your site via caching.

Read post

One of the techniques that makes a good developer great is the application of intelligent and level of caching. Caching reduces redundant work on the server in the interest of creating a more performant experience for the end user while saving CPU cycles and other costly resources.

Caching is one of the dark corners of SilverStripe Framework, in the sense that it isn’t used on as many projects as it should be, and many developers don’t even know it’s there. It’s easy to get started, though, and even if you just identify a few key areas of your site that can be cached, your users will be much better for it.

Why do we need a new cache module?

The cache layer that ships with SilverStripe Framework is robust and functional, so it’s natural to be suspect of anything that claims to reinvent the wheel. In fact, we won’t be talking about a wholesale reinvention of caching in SilverStripe. It's the partial caching — the definition of cached template blocks — that can be improved, because partial caching in SilverStripe, while useful in its own right, has a notable flaw: it’s database driven.

Take a look at this elementary example of a cache block, taken straight from the SilverStripe documentation:

<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
    <% loop $Menu(1) %>
    ...
    <% end_loop %>
<% end_cached %>

This declarative cache block can be nested in any template that displays the top-level navigation. The logic is clear – invalidate the cache when any SiteTree object is edited, deleted, or created. The first argument, 'navigation', is an arbitrary name for the cache block. The following arguments are concatenated to produce a SKU, or composite key that is guaranteed to change based on the state that you’re querying (in this case, any mutation of a SiteTree record).

This works great, but it still requires a database query, and that comes with inherent overhead. The cost of that overhead will vary with the size of your database, so it’s fair to say that this approach doesn’t scale as well as it could.

Cache invalidation is hard

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

The problem is, using the database to invalidate cache is really effective. It’s a live, to-the-microsecond view of your application state. This is a good thing, because delivering stale state in exchange for performance gains is not exactly optimal.

Any alternative to database-driven cache keys, therefore, would have to afford a very granular level of control.

Solution: Heyday’s silverstripe-cacheinclude module

The silverstripe-cacheinclude module works fundamentally differently than SilverStripe’s default partical caching. Rather than query the database, it follows an event-driven approach to cache invalidation. When a DataObject is created or mutated, the cache module evaluates whether those changes merit an invalidation. Further, the HTTP request can influence the cache key, which affords more options for caching things like search results.

Let’s look at a typical set up. First, install the invalidation extension on all DataObjects.

DataObject:
  extensions:
    - Heyday\CacheInclude\SilverStripe\InvalidationExtension

Now let’s set up some basic cache configurations. Typically, you can do this in one location, e.g. mysite/_config/caching.yml.

Injector:
  CacheIncludeConfig:
    class: Heyday\CacheInclude\Configs\ArrayConfig
    properties:
      Config: {}

We’ll leave the Config block empty for now. First, we just need to define the class that will provide the cache configuration service. For backward compatability, the module offers an alternative YamlConfig service that read YAML files before SilverStripe 3.

Defining cache blocks

Let’s fill out that Configs property, where all of our cache blocks will be defined.

Basic caching

Injector:
  CacheIncludeConfig:
    class: Heyday\CacheInclude\Configs\ArrayConfig
    properties:
      Config:
        BlogPosts:
          contains:
            - BlogPost
          context: no          

This cache block invalidates when ever a BlogPost object is updated. It can be pulled into the template using a standard partial cache block.

<% cache 'BlogPosts' %>
    <% loop $BlogPosts %>
        ...
    <% end_loop %>
<% end_cache %>

Adding context

What happens if we have multiple blogs? We'll need to manage separate caches for each blog. For that, we can use context.

      Config:
        BlogPosts:
          contains:
            - BlogPost
          context: page

Now, the cache key is URL sensitive, meaning /blogs/blog-1/ will cache the block separately from /blogs/blog-2.

What if we have filtered views of our blog, such as year, month, or search?

      Config:
        BlogPosts:
          contains:
            - BlogPost
          context: full

By using context: full the entire URL, including search parameters is examined to generate the cache key.

Dealing with logged-in state

Sometimes, the contents of a cached block will vary depending on the logged-in state. For instance, you may want to provide a dashboard for each member.

      Config:
        BlogPosts:
          contains:
            - BlogPost
          context: full
        Dashboard:
          member: true
          contains:
            - PurchasedItem

Forced expiry

For cached blocks that have some tolerance for staleness, you can use a simple expires property.

      Config:
        WeatherInTokyo:
          expires: '+1 hour'

Random sequences

If you’ve applied something like ->sort('RAND()') on your list, you probably understand that caching such a result set can be troubling. For views like this, you can define a fixed number of versions you want to create.

      Config:
        HomePageSlideshow:
          contains:
            - HomePageSlide
          versions: 5
          expires: '+1 hour'

This cache block definition will create five different incarnations of the randomised slides. Even if you have 100 slides, the user will only ever see five arrangements, until the expiry spawns five new sets.

Power users

A lot of the value in the silverstripe-cacheinclude module is tucked away in its advanced features.

Full request caching

It can be very tricky to get this right, but the performance gains are tremendous. The module allows you to cache full request objects, meaning you can generate a response without ever bootstrapping the framework.

This example, taken straight from the README demonstrates how to do create this complex logic using Symfony’s expression language.

Injector:
  RequestProcessor:
    class: RequestProcessor
    properties:
      filters:
        - '%$RequestCache'

  RequestCache:
    class: Heyday\CacheInclude\RequestCache
    constructor:
      0: '%$CacheInclude'
      1: '%$CacheIncludeExpressionLanguage'
      2: Global
    properties:
      # Expression language rules:
      # Add here any rules that should cause a request to not have a cache saved
      SaveExcludeRules:
        - 'request.getUrl() matches "{^admin|dev|cache-manager}"'

      # Add here any rules that must pass in order for a request to have a cache saved
      SaveIncludeRules:
        - "request.httpMethod() == 'GET'"
        - "response.getStatusCode() == 200"

      # Add here any rules that should cause a request to not have a cache served
      FetchExcludeRules:
        - 'request.getUrl() matches "{^admin|dev|cache-manager}"'

      # Add here any rules that must pass in order for a request to have a cache served
      FetchIncludeRules:
        - "request.httpMethod() == 'GET'"

The cache manager UI

A near automatic add-on for the silverstripe-cacheinclude module is the silverstripe-cacheinclude-manager module, which provides a ReactJS driven backend, displaying all cache keys and their current state, along with forced invalidation. The UI is updated in real time via long polling, and can be tremendously useful in testing.

Screenshot 2016 03 22 15.18.51

Give it a try!

For more information on the silverstripe-cacheinclude module, visit the Github page. Speed up your site, and cache in!

Header image by Ozzy Delaney

About the author
Aaron Carlino

Aaron Carlino, better known by his whimsical pseudonym Uncle Cheese has been an active member of the SilverStripe community since 2007, and has never looked back. In that time, he has established himself as a support resource, mentor, and contributor of some of the framework's most popular open source modules.

Post your comment

Comments

No one has commented on this page yet.

RSS feed for comments on this page | RSS feed for all comments

Like what you have read?

Sign up for our weekly blog digest sent to your inbox.

Subscribe