Skip to content

Commit

Permalink
API Use symfony/cache (fixes silverstripe#6252)
Browse files Browse the repository at this point in the history
  • Loading branch information
chillu committed Feb 25, 2017
1 parent 84ee2c1 commit 05de2ab
Show file tree
Hide file tree
Showing 24 changed files with 549 additions and 498 deletions.
8 changes: 1 addition & 7 deletions _config.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use SilverStripe\Core\Cache;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Dev\Deprecation;
use SilverStripe\View\Parsers\ShortcodeParser;

Expand Down Expand Up @@ -32,12 +32,6 @@
// @todo
// ->register('dbfile_link', array('DBFile', 'handle_shortcode'))

// Zend_Cache temp directory setting
$_ENV['TMPDIR'] = TEMP_FOLDER; // for *nix
$_ENV['TMP'] = TEMP_FOLDER; // for Windows

Cache::set_cache_lifetime('GDBackend_Manipulations', null, 100);

// If you don't want to see deprecation errors for the new APIs, change this to 3.2.0-dev.
Deprecation::notification_version('3.2.0');

Expand Down
22 changes: 22 additions & 0 deletions _config/cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
Name: corecache
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Cache\CacheFactory:
class: 'SilverStripe\Core\Cache\DefaultCacheFactory'
constructor:
directory: `TEMP_FOLDER`
Psr\SimpleCache\CacheInterface.GDBackend_Manipulations:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "GDBackend_Manipulations"
defaultLifetime: 100
Psr\SimpleCache\CacheInterface.cacheblock:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "cacheblock"
defaultLifetime: 600
Psr\SimpleCache\CacheInterface.LeftAndMain_CMSVersion:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "LeftAndMain_CMSVersion"
10 changes: 5 additions & 5 deletions admin/code/LeftAndMain.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Cache;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
Expand Down Expand Up @@ -2020,9 +2020,9 @@ public function CMSVersion()
// Tries to obtain version number from composer.lock if it exists
$composerLockPath = BASE_PATH . '/composer.lock';
if (file_exists($composerLockPath)) {
$cache = Cache::factory('LeftAndMain_CMSVersion');
$cacheKey = filemtime($composerLockPath);
$versions = $cache->load($cacheKey);
$cache = Injector::inst()->get(CacheInterface::class . '.LeftAndMain_CMSVersion');
$cacheKey = (string)filemtime($composerLockPath);
$versions = $cache->get($cacheKey);
if ($versions) {
$versions = json_decode($versions, true);
} else {
Expand All @@ -2038,7 +2038,7 @@ public function CMSVersion()
$versions[$package->name] = $package->version;
}
}
$cache->save(json_encode($versions), $cacheKey);
$cache->set($cacheKey, json_encode($versions));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"symfony/yaml": "~2.7",
"embed/embed": "^2.6",
"swiftmailer/swiftmailer": "~5.4",
"symfony/cache": "^3.3@dev",
"symfony/config": "^2.8",
"symfony/translation": "^2.8",
"vlucas/phpdotenv": "^2.4"
Expand Down
3 changes: 1 addition & 2 deletions docs/en/02_Developer_Guides/01_Templates/07_Caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ When we render `$Counter` to the template we would expect the value to increase
## Partial caching

Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data
from the database to display, the contents of the area are fetched from the `TEMP_FOLDER` file-system pre-rendered and
ready to go. More information about Partial caching is in the [Performance](../performance) guide.
from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching).

:::ss
<% cached 'MyCachedContent', LastEdited %>
Expand Down
279 changes: 155 additions & 124 deletions docs/en/02_Developer_Guides/08_Performance/01_Caching.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Caching

## Built-In Caches
## Overview

The framework uses caches to store infrequently changing values.
By default, the storage mechanism is simply the filesystem, although
other cache backends can be configured. All caches use the [api:Cache] API.
By default, the storage mechanism chooses the most performant adapter available
(PHP7 opcache, APC, or filesystem). Other cache backends can be configured.

The most common caches are manifests of various resources:

Expand All @@ -21,136 +21,167 @@ executing the action is limited to the following cases when performed via a web
* A user is logged in with ADMIN permissions
* An error occurs during startup

## The Cache API
## Configuration

The [api:Cache] class provides a bunch of static functions wrapping the Zend_Cache system
in something a little more easy to use with the SilverStripe config system.
We are using the [PSR-16](http://www.php-fig.org/psr/psr-16/) standard ("SimpleCache")
for caching, through the [symfony/cache](https://symfony.com/doc/current/components/cache.html) library.
Note that this library describes usage of [PSR-6](http://www.php-fig.org/psr/psr-6/) by default,
but also exposes caches following the PSR-16 interface.

A `Zend_Cache` has both a frontend (determines how to get the value to cache,
and how to serialize it for storage) and a backend (handles the actual
storage).
Cache objects are configured via YAML
and SilverStripe's [dependency injection](/developer-guides/extending/injector) system.

Rather than require library code to specify the backend directly, cache
consumers provide a name for the cache backend they want. The end developer
can then specify which backend to use for each name in their project's
configuration. They can also use 'all' to provide a backend for all named
caches.
:::yml
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.myCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "myCache"

End developers provide a set of named backends, then pick the specific
backend for each named cache. There is a default File cache set up as the
'default' named backend, which is assigned to 'all' named caches.
Cache objects are instantiated through a [CacheFactory](SilverStripe\Core\Cache\CacheFactory),
which determines which cache adapter is used (defaulting to a filesystem-backed cache).
This factory allows us you to globally define an adapter for all cache instances.

## Using Caches
:::php
use Psr\SimpleCache\CacheInterface
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');

Caches can be created and retrieved through the `Cache::factory()` method.
The returned object is of type `Zend_Cache`.
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 dependant on the used adapter: While the `FilesystemCache` adapter clears only the namespaced cache,
a `MemcachedCache` adapter will clear all caches regardless of namespace, since the underlying memcached
service doesn't support this. See "Invalidation" for alternative strategies.

:::php
// foo is any name (try to be specific), and is used to get configuration
// & storage info
$cache = Cache::factory('foo');
if (!($result = $cache->load($cachekey))) {
$result = caluate some how;
$cache->save($result, $cachekey);
}
return $result;

Normally there's no need to remove things from the cache - the cache
backends clear out entries based on age and maximum allocated storage. If you
include the version of the object in the cache key, even object changes
don't need any invalidation. You can force disable the cache though,
e.g. in development mode.

:::php
// Disables all caches
Cache::set_cache_lifetime('any', -1, 100);
## Usage

You can also specifically clean a cache.
Keep in mind that `Zend_Cache::CLEANING_MODE_ALL` deletes all cache
entries across all caches, not just for the 'foo' cache in the example below.
Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class interface.

:::php
$cache = Cache::factory('foo');
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);

A single element can be invalidated through its cache key.

:::php
$cache = Cache::factory('foo');
$cache->remove($cachekey);

use Psr\SimpleCache\CacheInterface
$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):

:::php
use Psr\SimpleCache\CacheInterface
$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:

:::php
use Psr\SimpleCache\CacheInterface
$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:

:::php
use Psr\SimpleCache\CacheInterface
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');

// remove the cache item
$cache->set('myCacheKey', 'myValue', 300); // cache for 300 seconds

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 ("TTL").
It defaults to 10 minutes (600s) in SilverStripe, which can be
quite short depending on how often your data changes.
Keep in mind that data expiry should primarily be handled by your cache key,
e.g. by including the `LastEdited` value when caching `DataObject` results.

:::php
// set all caches to 3 hours
Cache::set_cache_lifetime('any', 60*60*3);

## Alternative Cache Backends

By default, SilverStripe uses a file-based caching backend.
Together with a file stat cache like [APC](http://us2.php.net/manual/en/book.apc.php)
this is reasonably quick, but still requires access to slow disk I/O.
The `Zend_Cache` API supports various caching backends ([list](http://framework.zend.com/manual/1.12/en/zend.cache.backends.html))
which can provide better performance, including APC, Xcache, ZendServer, Memcached and SQLite.

## Cleaning caches on flush=1 requests

If `?flush=1` is requested in the URL, e.g. http://mysite.com?flush=1, this will trigger a call to `flush()` on
any classes that implement the `Flushable` interface. Using this, you can trigger your caches to clean.

See [reference documentation on Flushable](/developer_guides/execution_pipeline/flushable/) for implementation details.

### Memcached

This backends stores cache records into a [memcached](http://www.danga.com/memcached/)
server. memcached is a high-performance, distributed memory object caching system.
To use this backend, you need a memcached daemon and the memcache PECL extension.

:::php
// _config.php
Cache::add_backend(
'primary_memcached',
'Memcached',
array(
'servers' => array(
'host' => 'localhost',
'port' => 11211,
'persistent' => true,
'weight' => 1,
'timeout' => 5,
'retry_interval' => 15,
'status' => true,
'failure_callback' => null
)
)
);
Cache::pick_backend('primary_memcached', 'any', 10);

### APC

This backends stores cache records in shared memory through the [APC](http://pecl.php.net/package/APC)
(Alternative PHP Cache) extension (which is of course need for using this backend).

:::php
Cache::add_backend('primary_apc', 'APC');
Cache::pick_backend('primary_apc', 'any', 10);

### Two-Levels

This backend is an hybrid one. It stores cache records in two other backends:
a fast one (but limited) like Apc, Memcache... and a "slow" one like File or Sqlite.

:::php
Cache::add_backend('two_level', 'Two-Levels', array(
'slow_backend' => 'File',
'fast_backend' => 'APC',
'slow_backend_options' => array(
'cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache'
)
));
Cache::pick_backend('two_level', 'any', 10);
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).

:::yml
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.

:::php
use Psr\SimpleCache\CacheInterface
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');

// Automatically changes when any group is edited
$cacheKey = implode(['groupNames', $member->ID, Groups::get()->max('LastEdited')]);
$cache->set($cacheKey, $member->Groups()->column('Title'));

If `?flush=1` is requested in the URL, this will trigger a call to `flush()` on
any classes that implement the [Flushable](/developer_guides/execution_pipeline/flushable/)
interface. Use this interface to trigger `clear()` on your caches.

## Adapters

SilverStripe tries to identify the most performant cache available on your system
through the [DefaultCacheFactory](api:SilverStripe\Core\Cache\DefaultCacheFactory) implementation:

* - `PhpFilesCache` (PHP 7 with opcache enabled)
* - `ApcuCache` (requires APC) with a `FilesystemCache` fallback (for larger cache volumes)
* - `FilesystemCache` if none of the above is available

The library supports various [cache adapters](https://github.com/symfony/cache/tree/master/Simple)
which can provide better performance, particularly in multi-server environments with shared caches like Memcached.

Since we're using dependency injection to create caches,
you need to define a factory for a particular adapter,
following the `SilverStripe\Core\Cache\CacheFactory` interface.
Different adapters will require different constructor arguments.
We've written factories for the most common cache scenarios:
`FilesystemCacheFactory`, `MemcachedCacheFactory` and `ApcuCacheFactory`.

Example: Configure core caches to use [memcached](http://www.danga.com/memcached/),
which requires the [memcached PHP extension](http://php.net/memcached),
and takes a `MemcachedClient` instance as a constructor argument.

:::yml
---
After:
- '#corecache'
---
SilverStripe\Core\Injector\Injector:
MemcachedClient:
class: 'Memcached'
calls:
- [ addServer, [ 'localhost', 11211 ] ]
SilverStripe\Core\Cache\CacheFactory:
class: 'SilverStripe\Core\Cache\MemcachedCacheFactory'
constructor:
client: '%$MemcachedClient

## Additional Caches

Unfortunately not all caches are configurable via cache adapters.

* [SSViewer](api:SilverStripe\View\SSViewer) writes compiled templates as PHP files to the filesystem
(in order to achieve opcode caching on `include()` calls)
* [ConfigManifest](api:SilverStripe\Core\Manifest\ConfigManifest) is hardcoded to use `FilesystemCache`
* [ClassManifest](api:SilverStripe\Core\Manifest\ClassManifest) and [ThemeManifest](api:SilverStripe\View\ThemeManifest)
are using a custom `ManifestCache`
* [i18n](api:SilverStripe\i18n\i18n) uses `Symfony\Component\Config\ConfigCacheFactoryInterface` (filesystem-based)
Loading

0 comments on commit 05de2ab

Please sign in to comment.