Object caching
Overview
Object caching stores infrequently changing values to improve performance.
By default, the storage mechanism chooses the most performant filesystem adapter available (PHP opcache, or filesystem) but does not use in-memory cache unless you configure it. Other cache backends can be configured.
Object caching is useful for situations where fetching or generating data can take a long time, such as expensive database queries or API calls to external services.
The most common caches are manifests of various resources:
- PHP class locations (ClassManifest)
- Configuration settings from YAML files (CachedConfigCollection)
- Language files (i18n)
Flushing the various manifests is performed through a GET
parameter (flush=1
) or CLI flag (--flush
). Since this action requires more server resources than normal requests,
executing the action is limited to the following cases when performed via a web request:
- The environment is in "dev mode"
- A user is logged in with ADMIN permissions
- An error occurs during startup
Caution: Not all caches are cleared through ?flush=1
or --flush
.
While cache objects can expire, when using filesystem caching the files are not actively pruned.
For long-lived server instances, this can become a capacity issue over time - see
workaround.
Configuration
We use the PSR-16 standard ("SimpleCache") for caching, through the symfony/cache library.
Silverstripe provides a PSR-16 interface via the Psr16Cache
class which wraps symfony/cache
's PSR-6 compliant adapters.
Cache objects are configured via YAML and Silverstripe CMS's dependency injection system.
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.myCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "myCache"
Please note that if you have the silverstripe/versioned
module installed (automatically installed by the
silverstripe/cms
module), caches will automatically be segmented by current “stage”. This ensures that
any content written to the cache in the draft reading mode isn’t accidentally exposed in the live reading mode.
Please read the versioned cache segmentation section for more information.
Cache objects are instantiated through a CacheFactory, which determines which cache adapter is used (see cache adapters for details). This factory allows us you to globally define an adapter for all cache instances.
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
Caches are namespaced, which might allow granular clearing of a particular cache without affecting others.
In our example, the namespace is "myCache", expressed in the service name as
Psr\SimpleCache\CacheInterface.myCache
. We recommend the ::class
short-hand to compose the full service name.
Clearing caches by namespace is dependent on the used adapter: While the FilesystemAdapter
clears only the namespaced cache,
a MemcachedAdapter
adapter will clear all caches regardless of namespace, since the underlying memcached
service doesn't support this. See "Invalidation" for alternative strategies.
Usage
Cache objects follow the PSR-16 class interface.
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// create a new item by trying to get it from the cache
$myValue = $cache->get('myCacheKey');
// set a value and save it via the adapter
$cache->set('myCacheKey', 1234);
// retrieve the cache item
if (!$cache->has('myCacheKey')) {
// ... item does not exists in the cache
}
Invalidation
Caches can be invalidated in different ways. The easiest is to actively clear the entire cache. If the adapter supports namespaced cache clearing, this will only affect a subset of cache keys ("myCache" in this example):
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// remove all items in this (namespaced) cache
$cache->clear();
You can also delete a single item based on it's cache key:
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// remove the cache item
$cache->delete('myCacheKey');
Individual cache items can define a lifetime, after which the cached value is marked as expired:
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// set a cache item with an expiry
// cache for 300 seconds
$cache->set('myCacheKey', 'myValue', 300);
If a lifetime isn't defined on the set()
call, it'll use the adapter default.
In order to increase the chance of your cache actually being hit,
it often pays to increase the lifetime of caches.
You can also set your lifetime to 0
, which means they won't expire.
Since many adapters don't have a way to actively remove expired caches,
you need to be careful with resources here (e.g. filesystem space).
---
Name: my-project-cache
After: '#corecache'
---
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.cacheblock:
constructor:
defaultLifetime: 3600
In most cases, invalidation and expiry should be handled by your cache key.
For example, including the LastEdited
value when caching DataObject
results
will automatically create a new cache key when the object has been changed.
The following example caches a member's group names, and automatically
creates a new cache key when any group is edited. Depending on the used adapter,
old cache keys will be garbage collected as the cache fills up.
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// Automatically changes when any group is edited
$cacheKey = implode(['groupNames', $member->ID, Group::get()->max('LastEdited')]);
$cache->set($cacheKey, $member->Groups()->column('Title'));
If ?flush=1
is requested in the URL or --flush
is used with sake, this will trigger a call to flush()
on
any classes that implement the Flushable
interface. Use this interface to trigger clear()
on your caches.
Versioned cache segmentation
SilverStripe\Core\Cache\CacheFactory
now maintains separate cache pools for each versioned stage (if you have the
silverstripe/versioned
module installed). This prevents developers from caching draft data and then
accidentally exposing it on the live stage without potentially required authorisation checks. Unless you
rely on caching across stages, you don't need to change your own code for this change to take effect. Note
that cache keys will be internally rewritten, causing any existing cache items to become invalid when this
change is deployed.
// Before:
$cache = Injector::inst()->get(CacheInterface::class . '.myapp');
Versioned::set_stage(Versioned::DRAFT);
$cache->set('my_key', 'Some draft content. Not for public viewing yet.');
Versioned::set_stage(Versioned::LIVE);
// 'Some draft content. Not for public viewing yet'
$cache->get('my_key');
// After:
$cache = Injector::inst()->get(CacheInterface::class . '.myapp');
Versioned::set_stage(Versioned::DRAFT);
$cache->set('my_key', 'Some draft content. Not for public viewing yet.');
Versioned::set_stage(Versioned::LIVE);
// null
$cache->get('my_key');
Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache
with the disable-container
argument.
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.myapp:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "MyInsensitiveData"
disable-container: true
Additional caches
Unfortunately not all caches are configurable via cache adapters.
SSTemplateEngine
writes compiled templates as PHP files to the filesystem (in order to achieve opcode caching oninclude()
calls)- i18n uses
Symfony\Component\Config\ConfigCacheFactoryInterface
(filesystem-based)