silverstripe-framework/docs/en/reference/partial-caching.md

242 lines
7.4 KiB
Markdown

# Partial Caching
## Introduction
Partial caching, new in SilverStripe 2.4, is a feature that allows the caching of just a portion of a page.
As opposed to static publishing, which avoids the SilverStripe controller layer on cached pages, partial caching allows
caching for pages that contain a mix of moderately static & user specific data, and still provide full access control
and permission enforcement.
The trade-off is that it does not provide as much performance improvement as static publishing, although for data heavy
pages the speed increases can be significant.
## Basics
The way you mark a section of the template as being cached is to wrap that section in a cached tag, like so:
:::ss
<% cached %>
$DataTable
...
<% end_cached %>
Each cache block has a cache key - an unlimited number of comma separated variables (in the same form as `if` and
`control` tag variables) and quoted strings.
Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is the
same as a previous render, the cached value stored last time is used.
Since the above example contains just one argument as the cache key, a string (which will be the same every render) it
will invalidate the cache after the TTL has expired (default 10 minutes)
Here are some more complex examples:
From a block that updates every time the Page subclass it's the template for updates
:::ss
<% cached 'database', LastEdited %>
From a block that shows a login block if not logged in, or a homepage link if logged in, depending on the current member
:::ss
<% cached 'loginblock', CurrentMember.ID %>
From a block that shows a summary of the page edits if administrator, nothing if not
:::ss
<% cached 'loginblock', LastEdited, CurrentMember.isAdmin %>
## Aggregates
Often you want to invalidate a cache when any in a set of objects change, or when the objects in a relationship change.
To help do this, SilverStripe 2.4 also introduces the concept of Aggregates. These calculate and return SQL aggregates
on sets of DataObjects - the most useful for us being the Max aggregate.
For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it
otherwise. By using aggregates, that's easy
:::ss
<% cached 'navigation', Aggregate(Page).Max(LastEdited) %>
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added or
edited
:::ss
<% cached 'categorylist', Aggregate(Category).Max(LastEdited) %>
We can also calculate aggregates on relationships. A block that shows the current member's favourites needs to update
whenever the relationship Member::$has_many = array('Favourites' => Favourite') changes.
:::ss
<% cached 'favourites', CurrentMember.ID, CurrentMember.RelationshipAggregate(Favourites).Max(LastEdited) %>
## Cache key calculated in controller
That last example is a bit large, and is complicating our template up with icky logic. Better would be to extract that
logic into the controller
:::php
function FavouriteCacheKey() {
$member = Member::currentUser();
return implode('_', array(
'favourites',
$member->ID,
$member->RelationshipAggregate('Favourites')->Max('LastEdited')
));
}
and then using that function in the cache key
:::ss
<% cached FavouriteCacheKey %>
## Cache blocks and template changes
In addition to the key elements passed as parameters to the cached control, the system automatically includes the
template name and a sha1 hash of the contents of the cache block in the key. This means that any time the template is
changed the cached contents will automatically refreshed.
## Purposely stale data
In some situations it's more important to be fast than to always be showing the latest data. By constructing the cache
key to invalidate less often than the data updates you can ensure rendering time is constant no matter how often the
data updates.
For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do
:::ss
<% cached 'blogstatistics', Blog.ID %>
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
configurable only on a site-wide basis), you could add a special function to your controller:
:::php
function BlogStatisticsCounter() {
return (int)(time() / 60 / 5); // Returns a new number every five minutes
}
and then use it in the cache key
:::ss
<% cached 'blogstatistics', Blog.ID, BlogStatisticsCounter %>
## Cache block conditionals
You may wish to conditionally enable or disable caching. To support this, in cached tags you may (after any key
arguments) specify 'if' or 'unless' followed by a standard template variable argument. If 'if' is used, the resultant
value must be true for that block to be cached. Conversely if 'unless' is used, the result must be false.
Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing
heavy load:
:::ss
<% cached 'blogstatistics', Blog.ID if HighLoad %>
By adding a HighLoad function to your page controller, you could enable or disable caching dynamically.
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
you could use something like:
:::ss
<% cached unless CurrentUser %>
As a shortcut, the template tag 'uncached' can be used - it is the exact equivilent of a cached block with an if
condition that always returns false. The key and conditionals in an uncached tag are ignored, so you can easily
temporarily disable a particular cache block by changing just the tag, leaving the key and conditional intact.
:::ss
<% uncached %>
## Nested cacheblocks
New since 2.4 beta 2 is the ability to nest independent cache blocks (with one important rule, discussed later).
Any nested cache blocks are calculated independently from their containing block, regardless of the cached state of that
container.
This allows you to wrap an entire page in a cache block on the page's LastEdited value, but still keep a member-specific
portion dynamic, without having to include any member info in the page's cache key.
An example:
:::ss
<% cached LastEdited %>
Our wonderful site
<% cached Member.ID %>
Welcome $Member.Name
<% end_cached %>
$ASlowCalculation
<% end_cached %>
This will cache the entire outer section until the next time the page is edited, but will display a different welcome
message depending on the logged in member.
Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you
could also write the last example as:
:::ss
<% cached LastEdited %>
Our wonderful site
<% uncached %>
Welcome $Member.Name
<% end_uncached %>
$ASlowCalculation
<% end_cached %>
## The important rule
Currently cached blocks can not be contained within if or control blocks. The template engine will throw an error
letting you know if you've done this. You can often get around this using aggregates.
Failing example:
:::ss
<% cached LastEdited %>
<% control Children %>
<% cached LastEdited %>
$Name
<% end_cached %>
<% end_control %>
<% end_cached %>
Can be re-written as:
:::ss
<% cached LastEdited %>
<% cached RelationshipAggregate(Children).Max(LastEdited) %>
<% control Children %>
$Name
<% end_control %>
<% end_cached %>
<% end_cached %>