Silverstripe-framework: SS4 caching: update Zend_Cache or PSR-6?

Created on 31 Oct 2016  Â·  39Comments  Â·  Source: silverstripe/silverstripe-framework

UPDATE: Ticket description has been extended/replaced by @chillu on 10th of Feb

Tasks

  • [x] Remove Zend_Cache dep
  • [x] Use DI for cache configuration
  • [x] Create factory for retrieving caches
  • [x] Fix tests
  • [x] Update documentation
  • [x] Summarise changes in changelog
  • [x] Create ticket for ManifestCache rewrite (should use same cache)
  • [x] Create ticket with symfony/cache about filesystem cache garbage collection (https://github.com/symfony/symfony/issues/21764)
  • [x] Investigate using PhpFile adapter rather than Filesystem (or use createSystemCache)
  • [x] Test PHP7 PhpFileCache operation (is 64M enough cache memory by default?)
  • [x] Resolve conflicts with https://github.com/silverstripe/silverstripe-framework/pull/6641 (if that one is merged first)

Goals

  • Replace outdated Zend_Cache implementation
  • Use a standards-based implementation (PSR-6 or PSR-16)
  • Minimise upgrade pain (changes to non-core cache uses)
  • Retain or improve cache access and write performance

Scope

  • Required

    • PSR-6/PSR-16: Store data in a cache pools

    • PSR-6/PSR-16: Store and retrieve common data types via cache keys

    • PSR-6/PSR-16: Define cache expiry on each item

    • PSR-6/PSR-16: Batched cache writes

    • PSR-6/PSR-16: Hit/miss detection

    • PSR-6/PSR-16: Invalidate all caches in a pool

    • Define named cache pools

    • Configure cache drivers (e.g. connection details)

    • Support different drivers

    • Configure through YAML (custom code can modify core caching behaviour)

    • Use for SilverStripe manifests (prior to YAML config being available)

  • Desireable

    • Configure cache drivers per cache pool

    • Cascading cache drivers

    • Stampede Protection

    • Cache Key Grouping

    • Support for at least one driver which can be shared between distributed webservers (e.g. memcache)

    • PSR-6: Deferred cache writes (separate commit phase)

    • Maintenance: Active cache purging (e.g. through cronjobs for filesystem caches)

  • Out of scope

    • Cache introspection (track hits/misses, list cached items)

Usage Comparison

// SS_Cache
$cache = SS_Cache::factory('myPool'); 
$itemValue = $cache->load('myKey');
$cache->save('myVal', 'myKey');

// PSR-6
$cache = new MyCacheAdapter();
$item = $cache->getItem('myKey');
$itemValue = $item->get();
$item->set('myVal');
$cache->save($item);

// PSR-16
$cache = new MyCacheAdapter();
$itemValue = $cache->get('myKey');
$cache->set('myKey', 'myVal');

Libraries

  • Symfony Cache

    • Both PSR-6 and PSR-16 (via adapters)

    • Dependencies: psr/cache, psr/log, psr/simple-cache

    • 4645 NCLOC

    • 290k composer installs

    • 26 github stars (it's a subtree split, expect less stars)

    • Tag-based cache invalidation (only with some adapters)

    • Cascading drivers through ChainAdapter

    • PSR-16 use is not documented

  • StashPHP

    • PSR-6

    • Dependencies: psr/cache

    • 3041 NCLOC

    • 600k composer installs

    • 731 github stars

    • Stampede protection (early cache regeneration, locking)

    • Cascading drivers through Composite

  • Doctrine Cache

    • Not PSR-6

  • Scrapbook Cash

    • PSR-6 and PSR-16

    • Dependencies: psr/cache, psr/simple-cache

    • 32k composer installs

    • 180 github stars

    • Stampede Protection

Options

Option 1a: Replace SS_Cache with a PSR-6 library

  • Pro: Standardization
  • Pro: Maximum flexibility (e.g. batch/deferred cache key writes)
  • Pro: Larger choice of libraries
  • Con: Slightly more code for standard cache operations
  • Con: Upgrade pain
  • Con: Directly exposing a PSR-6 library makes us dependant on additional APIs exposed by them (such as lock() in stashphp)

Option 1b: Adapt SS_Cache to use a PSR-6 library internally

  • Pro: Less upgrade pain
  • Con: API doesn't cleanly translate (no distinction between frontend/backend, or frontendOptions parameters)

Option 2a: Replace SS_Cache with a PSR-16 library ("simplecache")

  • Pro: Less code for standard cache operations
  • Con: Less library choice
  • Con: Upgrade pain

Option 2b: Adapt SS_Cache with a PSR-16 library internally

  • Pro: Less upgrade pain
  • Con: No point using a simplified cache API if it's only used "behind the scenes"
  • Con: API doesn't cleanly translate (same as Option 1b)

Option 2c: Refactor SS_Cache to become a PSR-16 library

  • Con: More code to maintain

Configuration

The majority of cached should be configured through YAML
(apart from the class and config manifest caches which need to be in place before YAML can be parsed).

This configuration leverages the SilverStripe dependency injection system
for configuring instances, alongside service names like Cache.driver.memcached.
The configuration assumes PSR-6 (referring to "pools"), but could be adapted to PSR-16.

Before:

_config.php

SS_Cache::add_backend(
    'filesystem', 
    'Filesystem',
    array(
        'path' => TEMP_FOLDER
    )
);
SS_Cache::pick_backend('filesystem', 'i18n', 10);
SS_Cache::set_cache_lifetime('i18n', 300);

SS_Cache::add_backend(
    'memcached', 
    'Memcached',
    array(
        'servers' => array(
            'host' => 'localhost', 
            'port' => 11211, 
        )
    )
);
SS_Cache::pick_backend('memcached', 'cacheblock', 10);

After:

config.yml

SilverStripe\Core\Injector\Injector:
  Cache.driver.default:
    class: 'My\Library\Drivers\Filesystem'
    constructor:
      path: `TEMP_FOLDER`
  Cache.driver.memcached:
    class: 'My\Library\Drivers\Memcached'
    constructor:
      host: localhost
      port: 11211
  Cache.pool.i18n:
    class: 'My\Library\CachePool'
    properties:
      driver: '%$Cache.driver.default' # optional, uses default
  Cache.pool.cacheblock:
    class: 'My\Library\CachePool'
    properties:
      driver: '%$Cache.driver.memcached'
i18n:
  cache_ttl: 300

Usage for class/config manifest (before YAML is available):

Core.php

$injector = new Injector(array('locator' => 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator'));
Injector::set_inst($injector);

// new
if (file_exists('boot-cache.php')) {
  require('boot-cache.php');
}

$manifest = new ClassManifest(BASE_PATH, false, $flush);
// ClassManifest->getCache() calls Cache::factory('Cache.pool.classmanifest')

boot-cache.php

Injector::inst()->registerService(
  new \My\Library\Drivers\Memcache('localhost', 11211)
  'Cache.pool.classmanifest'
);

Note: Additional boot files could be handled in a more generic fashion,
this just aims to illustrate the principle (inject procedural code early during boot).
It's going to be different to handle this via environment variables alone,
since different drivers have different construtor arguments, each of which
would need to be reflected in environment variables.

Pool Management

In Option 1a and 2a, SilverStripe developers are expected to directly interact with
a PSR-6 or PSR-16 library.

Before:

$cache = Cache::factory('i18n', 'myFrontend', array(
    'automatic_serialization' => true,
    'lifetime' => null
));
$itemValue = $cache->load('myKey');

After (with PSR-6):

$cache = Cache::factory('i18n');
$item = $cache->getItem('myKey');
$itemValue = $item->get();

If different drivers ('frontends') could be set on the cache pool instance,
a direct choice via the factory() should be avoided.
Most use cases should be covered by configuring drivers and driver
options per cache pool definition, not per cache pool instance.

Notes

  • Terminology: Zend calls it a "backend", Symfony an "adapter", and Stash a "driver"
  • The silverstripe/cacheinclude module is an alternative to partial template caches, and uses doctrine/cache. A nice side effect of this library is that you can list and expire individual cache keys via a manager user interface
  • Batch/deferred cache key setting might become important for external cache backends (e.g. memcache), combined with granular cache use. Currently, @micmania1's config rewrite sets a cache key per config block, which would be extremely slow when performed without batching with memcached network requests. Both PSR-6 and PSR-16 support this.
  • Cache expirations wouldn't be configured for the whole the pool, since they need to be passed into each item in both PSR-6 (CacheItemInterface->expiresAfter()) and PSR-16 (CacheInterface->function set($key, $value, $ttl = null)). If APIs like i18n want to expose one global value, they should make it configurable in their own namespace (e.g. i18n.cache_ttl)
  • Without a wrapper around the PSR-6/PSR-16 libs, we can't inject additional behaviour
    like logging cache hits, or providing structured data about cache access to a developer toolbar. StashPHP has a setLogger() method
  • ircmaxells post about PSR caching is worth reading (some motivations for PSR-16): "As simple as possible, as complex as necessary."

Further reading

Related Issues

affectv4

All 39 comments

I think we should switch to using PSR-6 for this, and then providing a default PSR-6 library as a framework dependency. This is what we did with PSR-3 / monolog, too.

I think that we should refactor SS_Cache to accept any PSR-6 implementation and ditch the Zend_Cache support.

This library seems to be one of the more popular PSR-6 implementations:
http://www.stashphp.com/

IIRC, one of the goals of simplecache was that it adds a thin API layer on top of PSR-6 to simplify it. Whether that’s something that the library would provide, I’m not sure - but I don’t think the two standards are mutually exclusive/competing.

+1 for PSR-6.

Another implementation is symfony/cache, but stash seems to be more popular (https://packagist.org/providers/psr/cache-implementation)

👍 for simplecache over psr-6

SimpleCache is a layer on top of PSR-6, that doesn't current have very widespread implementations. So "SimpleCache over PSR-6" doesn't really make sense. Using SimpleCache means using PSR-6.

That said, I think that SS classes directly accessing PSR-6 is a bit overblown. There needs to be some layer between the two, and we have a few choices for that:

  • Continue to use SS_Cache in more or less its current form, with SS_Cache connecting to a PSR-6 backend
  • Remove SS_Cache and replace it with a mechanism for fetching a SimpleCache class. The uncertainty here is that there aren't many popular SimpleCache implementation yet.
  • Refactor SS_Cache to _become_ a SimpleCache implementation. Remember that SimpleCache doesn't actually do any caching itself, it's syntatic sugar for PSR-6. This might be appropriate if the existing SimpleCache implementations aren't sufficiently lightweight.

This is the only SimpleCache implementation, apparently: http://www.scrapbook.cash/. It provides it as part of a caching library.

At 20,000 Packagist downloads it's less popular than Stash and Symfony cache.

Surely the point of using a PSR-6 caching lib is that we can use SS_Cache and have it powered by any PSR-6 compliant library? So why not just do that and we pick a default backend (symfony seems fine to me)...

If there are no SimpleCache implementations yet, my vote goes to refactoring SS_Cache to be one (assuming it’s not going to be a massive undertaking - which reading between the lines it shouldn’t be). If SimpleCache implementations are then released by other maintainers later, it’ll be a piece of cake to drop SS_Cache and switch to one of them.

So, what it sounds like the implication of what Loz is suggesting is:

  • Cache::factory() returns a Psr\SimpleCache\CacheInterface object (https://github.com/php-fig/simplecache/blob/master/src/CacheInterface.php)
  • There's a way of configuring different cache backends by name, proviidng PSR-6 cache pools (https://github.com/php-fig/cache/blob/master/src/CacheItemPoolInterface.php)
  • The configuration arguments (Cache::set_cache_lifetime() and the $frontend and $frontendOptions arguments of Cache::factory()) no longer work.

To be honest I would probably split out our simplecache implementation as silverstripe/simplecache – it may be of use to other developers, given that a standalone PSR-16 implementation doesn't currently exist (it only comes bundled with another caching library). It would be a PSR-16 compatible wrapper around a PSR-6 caching back-end.

What's not decided yet is how we define the caches. We could provide injected services such as SilverStripe\Core\Cache.<backend-name>. Or we could provide a range of injected services that live within the namespaces of the subsystems using the caches. So, for example, i18n::get_cache() could use the service SilverStripe\i18n\i18n\Cache.

In either case, it also begs the question of whether we need Cache::factory() or we should just get the services from the Injector directly. It seems like an unnecessary extra layer.

For reference, these are the caches that we use in core:

  • LeftAndMain_CMSVersion: Cache the CMS version derived from composer.lock
  • CMSMain_SiteTreeHints: Cache the calculation of CMSMain::SiteTreeHints
  • GDBackend_Manipulations: Retain the pass/fail state of manipulation calls between executions
  • SS_Configuration: Cache the ConfigManifest
  • i18n: Passed to Zend_Translate
  • cacheblock: Cache partial caches

Just to confirm: the new Cache and associated configuration API would be specifically designed for whichever PSR-6 library we choose to bundle with framework, right? Rather than attempting to be generic enough to be able to configure other PSR-6 libraries.

What's not decided yet is how we define the caches. We could provide injected services such as SilverStripe\Core\Cache.<backend-name>. Or we could provide a range of injected services that live within the namespaces of the subsystems using the caches. So, for example, i18n::get_cache() could use the service SilverStripe\i18n\i18n\Cache.

My preference is the latter - my understanding of the first option is that we couldn’t then (easily) set different configuration options for different uses of the same backend. Let’s say I want to override core caches to cache everything with Apc, and have different ttl values for i18n vs the config manifest - could I achieve that with the first option?

In either case, it also begs the question of whether we need Cache::factory() or we should just get the services from the Injector directly. It seems like an unnecessary extra layer.

I think it could still be useful as a convenience layer. We could use Injector to map short names for drivers to full classes:

$cache = SilverStripe\Cache\Cache::factory('Apc', ['ttl' => 600]);

// vs...

$cache = SilverStripe\Core\Injector\Injector::inst()->get('SilverStripe\Core\Cache.Apc');
$cache->getDriver()->setOptions(['ttl' => 600]);

There’s not a huge difference there, but it should be a pretty thin API layer to maintain for the added convenience.

Just to confirm: the new Cache and associated configuration API would be specifically designed for whichever PSR-6 library we choose to bundle with framework, right? Rather than attempting to be generic enough to be able to configure other PSR-6 libraries.

If we provided a configuration API then I would say so, but my preference would be that we just pass responsibility for configuration over to the 3rd part API and don't attempt to build any kind of configuration adapter at all — I don't think it would be a value-adding layer.

I think it could still be useful as a convenience layer. We could use Injector to map short names for drivers to full classes: $cache = SilverStripe\Cache\Cache::factory('Apc', ['ttl' => 600]);

I don't think the TTL argument here is appropriate. I think that the TTL setting should be part of the service definition for the APC cache.

This is the only SimpleCache implementation, apparently: http://www.scrapbook.cash/. It provides it as part of a caching library.

Looks like it’s a SimpleCache implementation specifically for scrapbook, but not a PSR-6 “wrapper” as such.

PSR-16/SimpleCache is under review at the moment, and these messages ([1] and [2]) seem to suggest that a PSR-6 to PSR-16 bridge will live in a simple-cache-utils package. If that’s the case, then we likely wouldn’t need to maintain our own implementation once (and assuming) PSR-16 is approved.

This might be a good time to come up with a solution for the cache cross-pollination issue raised by Nicolaas - we basically need a way for the system to influence the cache key, e.g. by adding a domain.

Having spent a bit of time working with caches in the last few days, but NOT having read all the stuff above or understanding it all ... Here are a few things I have thought of:

  • It would be great if you could change out SS_Cache class for something else. If SS_Cache were to extend Object then that is easy AFAIK (using a custom class directive).

  • In my work, I have not been too worried about cache expiry times. Basically, I would not let caches expire by themselves, but a flush would remove ALL of them. The idea that you would say: this cache expires in an hour and this one in two hours just seems rather complicated to fully understand in its implications and management. Rather than cache key expiry, you could perhaps simply overwrite cache values, clear it or point to new ones.

  • In my frustration working with memcache (https://github.com/silverstripe/silverstripe-framework/issues/6383), I started to write my own cache and came across the idea of using a Mysql table with set length fields (CHAR) stored using Engine=MEMORY (a MYSQL table that is stored in RAM in its entirety) together with an InnoDB table for longer strings. I like this idea as the default cache for Silverstripe because:
    -- (a) Mysql Memory table is exactly made for such a thing.
    -- (b) MYSQL is available and there is no need to set up anything additional (such as REDIS / Memcache)
    -- (c) it is super easy to inspect the values being stored (where reading values in, for example, memcache takes a lot more effort).
    -- (d) I imagine it to be faster and easier to work with than the current default of temp files.
    -- (e) as a database is always linked to one site, there is no question about cross-pollination that you may have with temp files and/or memcache
    This is only an idea right now, but . I would be super happy to research it if any of you thought that this idea might be a good one.

  • A caching system should be a storage place for keys with associated values (like a one dimensional associative array) that you are happy to lose at any time (flush, server reset) ... nothing more. Keeping it really simple will make it faster and easier to understand.

Sorry for the long rant. I hope it helps in getting a better caching system soon ;-)

@sunnysideup I appreciate you’ve had a few frustrating issues with caching recently, thanks for your work in tracking them and their causes down. We might not be able to fix them all, or fix them cleanly, in the 3.x releases but it’s helpful to know what problems we need to avoid in future.

I won’t touch on all of your points above, but we’ll definitely avoid writing our own caching system from scratch. Caching is deceptively hard to get right, and there are many mature, well-tested libraries out there that handle it very well (doctrine/cache, symfony/cache, tedivm/stash to name a few). If you’re thinking of writing your own for use in a live project, I would recommend instead investing the time in using a library like one of those listed above (of course, it could still be a fun side project to play around with!).

@kinglozzer - thank you for your reply. Totally agree with the maxim of using something existing and that being better than homebaked.

Having said that, you should consider this: _because the current caching system is hard to understand and tries to allow for so many options, with different settings, etc... it basically meant that SS 3 did not have a proper caching system beyond file caching_.

In other words, you may move the grunt work of the caching to a third-party system, but if that third-party system is complex and has many options then you may end up writing a larger amount of code to deal with the options then the amount of code required to write our own solution.

Thus, I would advocate for something really fast and simple with the ability to switch it out for something more complex.

One problem you may (probably will) encounter is that there's no way (or its hard/hacky) to change to a different cache backend.

Currently you can do:

define('SS_MANIFEST_CACHE', 'MyCustomClass');

That works perfectly fine for Filesystem cache or APC, but as soon as you want to use something like redis which requires configuration, this falls over.

eg.

$adapter = new FilesystemAdapter;

$redisClient = new Predis\Client([ // .. some config ... ]);
$adapter = new RedisAdapter($redisClient);

The user doesn't really have a way to configure redis and then pass that redis client into the adaptor.

This is one example but there's lots of other cache backends which have different dependencies and require different configuration. https://github.com/symfony/cache/tree/master/Adapter

Using config won't be possible here as config will also rely on the cache to have already been set up.

There's probably a wider discussion on bootstrapping needed but I thought I'd just leave this brain dump here for now.

I think that we should assume that for things other than the cache of config, config will be available to define the cache set-up.

Config can have a more hard-coded cache set-up.

As a first cut, we can just dish out PSR-6 CachePool objects from the Injector, and have a hardcoded PSR-6 CachePool object in Core.php for the config cache.

I've updated the description of this ticket to reflect some recommendations, and sprinkle in my own research (and opinions). Personally I think exposing SilverStripe devs to PSR-6 vs. the simpler PSR-16 isn't a big deal, it's three instead of two lines for an average cache key lookup/write. That being said, Symfony cache supports both. I'm tending towards StashPHP (PSR-6 only), because of nice features like cache stampede protection.

I used stash briefly in my config work but found it to be quite slow and didn't have as many cache backend options as symfony.

so +1 for symfony cache.

@micmania1 Did you compare this with Symfony Cache on the same workload afterwards? Both libs have roughly the same amount of code, so intuitively I wouldn't expect there to be a lot of performance difference.

@silverstripe/core-team We're planning to implement this in the next days. Can you please state your preference for one of the outlined options in the updated ticket descriptions, and whether you have a library preference? Also looking for feedback on the configuration and pool management approach.

So far, we have the following opinions (please let me know if I'm misrepresenting).

Standards:

  • PSR-6: @dhensby, @tractorcow
  • PSR-16/simplecache: @kinglozzer, @chillu, @sminnee

Libraries:

  • Symfony Cache: @micmania1, @dhensby, @chillu

Options:

  • Option 1a: Replace SS_Cache with a PSR-6 library: @sminnee (?)
  • Option 1b: Adapt SS_Cache to use a PSR-6 library internally: @dhensby, @tractorcow
  • Option 2a: Replace SS_Cache with a PSR-16 library ("simplecache"): @chillu, @kinglozzer (changed from "Option 2c" because we now have simplecache libraries available)

Note that I've changed my opinion to use PSR-16 and Symfony Cache, since I've realised that Symfony Cache implements both standards. In reality, the lack of Stampede Protection in Symfony Cache isn't a big deal, since sites needing it can simply extend the built in cache pool interface (see "No way to handle stampede protection" in ircmaxell's post)

I'd be okay with something like:

  • Different caches are identified as named services
  • Cache services must comply with PSR-16 (or PSR-6, happy to go with Ingo's preference)
  • We bundle symfony/cache as a framework dependency, as we do with monolog
  • Our default yaml defines symonfy/cache services for our default caches

Basically, give it the same treatment that we gave logging.

I’m still voting for option 2a. I think whichever library we choose, it’s going to offer enough flexibility for 99.9%. For the 0.01% that do want a different library, they can either roll their own code if it’s “userland” or write a PSR-16 adapter if they really need it for SS_Cache. That may be more difficult for some libraries than others, but at least it’s possible - so it’s a significant improvement on the status quo. I’m happy with Symfony too

I'm changing my vote to PSR-6 and symfony/cache.

Also vote for Option 1b: Adapt SS_Cache to use a PSR-6 library internally:, where we use SS_Cache as a factory for generating a cache "using whatever we've declared as the default". E.g. a Filesystem cache with a standard cache path set by silverstripe. Via config we could also DI another cache for this factory to generate by default, or allow named services to be used as the base for new caches.

OK so based on chats I've updated standards preferences to be:

  • PSR-6: @dhensby, @tractorcow
  • PSR-16/simplecache: @kinglozzer, @chillu, @sminnee

So it sounds like the main point of contention is retaining SS_Cache as an API (Option 1b and 2b, two votes), or replacing it with PSR-6/PSR-16 (Option 1a and 2a, three votes).

Looking at the SS_Cache API, we have the following methods:

  • add_backend($name, $type, $options = array()
  • pick_backend($name, $for, $priority = 1)
  • get_cache_lifetime($for)
  • set_cache_lifetime($for, $lifetime = 600, $priority = 1)
  • factory($for, $frontend = 'Output', $frontendOptions = null) (returns a non-PSR Zend_Cache object)

Just to clarify, is anybody suggesting to create a "PSR-6/PSR-16 to Zend_Cache" adapter, to avoid any existing cache users changing their code? Or is this just about retaining SS_Cache::factory() as an entry point and wrapper around a DI service creation based on the $for argument, while still returning a PSR-6/PSR-16 cache interface?

Sorry, the wording of the options ("internally") implied that we weren't requiring devs to change their own code use, and retain the Zend_Cache_Core signature: load($id, $doNotTestCacheValidity = false, $doNotUnserialize = false) and save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8). That doesn't sound feasible, so even Option 1b and 2b would expose PSR-6/PSR-16 cache interfaces to SilverStripe devs.

Ingo I think that level of detail is best addressed at the PR stage. Might be best to start coding?

I'm becoming less attached to the idea of keeping SS_Cache. What I was concerned with was making sure we have a unified API where backends are easily swapped out. It sounds like that can be solved without keeping SS_Cache, right?

If so, I'm happy to go with the crowd on this one

The reason I'd be against keeping SS_Cache is that PSR's are there to agree upon an API and if we add our own layer, we're not really buying into PSR's. Also, it means devs coming to SS would need to learn our API instead of using what they already know.

I'm becoming less attached to the idea of keeping SS_Cache. What I was concerned with was making sure we have a unified API where backends are easily swapped out. It sounds like that can be solved without keeping SS_Cache, right?

I was imagining something like:

$partialCache = Injector::inst()->get('Psr\Cache\CacheItemPoolInterface.Partials');
$item = $partialCache->getItem('foo');
if($item->isHit()) {
  return $item->get();
} else {
  $value = doSomething();
  $item->set($value);
  return $value;
}

and your yaml

Injector:
  Psr\Cache\CacheItemPoolInterface.Partials
    class: bla
    arguments:
     - foo
     - bar
     - bz

Instead of the base service name Psr\Cache\CacheItemPoolInterface we could just use Cache.

Status update: I've started implementing this and hitting some conceptual road blocks. I've identified a few implied requirements from the 3.x-style API:

  1. Globally exchange cache driver for all cache implementations without knowledge of which named caches exist
  2. Set different cache driver for one particular named cache
  3. Set default cache lifetime without duplicating all configuration for the cache driver(s)
  4. Allow creating a new named cache without writing a bunch of boilerplate YAML config

In Symfony's PSR-16 cache drivers, this is achieved by $namespace and $defaultLifetime constructor args. These differ based on implementation (argument position).

FilesystemCache::__construct($namespace = '', $defaultLifetime = 0, $directory = null)
MemcachedCache::__construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)

Every cache has it's own namespace in order to be clearable without affecting other caches (cacheblock, GDManipulation, LeftAndMain_Version, etc). In the filesystem driver, this translates to subfolders in TEMP_FOLDER. Because of the constructor arg use in Symfony (rather than setters), we need to define an entire YAML injector configuration for each of these variations. Here's a mock up of how this would look - using different drivers to illustrate the issue, but it would be the same amount of config when using the same driver for both (less with some YAML referencing magic):

Name: corecache
---
SilverStripe\Core\Injector\Injector:
  Cache.default:
    class: 'Symfony\Component\Cache\Simple\FilesystemCache'
    constructor:
      namespace: 'default'
      defaultLifetime: 300
      directory: `TEMP_FOLDER`
  Cache.GDBackend_Manipulations:
    class: 'Symfony\Component\Cache\Simple\MemcachedCache'
    constructor:
      client: '%$MemcachedCacheService
      namespace: 'GDBackend_Manipulations'
      defaultLifetime: 100

Namespace option 1: Create lightweight driver-specific factories return PSR-16 objects, e.g. SilverStripe\Core\Cache\FilesystemCacheFactory and SilverStripe\Core\Cache\MemcacheCacheFactory. These would pass through constructor args, but also take a $namespace argument separately in order to insert it in the correct place depending on the constructor method signature.

Namespace option 2: Use StashPHP + PSR-6, which means we can use Pool->setNamespace() and consistently rely on the SS Injector with a minimal Cache::factory() wrapper to create these services. Since StashPHP doesn't support PSR-16, this automatically means exposing PSR-6 to SilverStripe devs (unless we want to write our own adapters).

Namespace option 3: Drop the "Allow creating a new named cache without writing a bunch of boilerplate YAML config" requirement. This would mean any project would need to know about all cache implementations used throughout core, modules and project code, and duplicate the same YAML config block for each of them to consistently set a different driver (e.g. exchanging filesystem for memcache).

I think that option 1 is the best road to go down. I'd define a CacheFactory interface for these factory objects, and have all the default cache services use something like this in their service definition

CacheFactory.php

namespace SilverStripe\Core\Cache;

interface CacheFactory
{
  function create($namespace, $lifetime = null);
}

cache.yml

SilverStripe\Core\Cache\CacheFactory:
  class: 'SilverStripe\Core\Cache\FilesystemCacheFactory'
  properties:
    Directory: `TEMP_FOLDER`
SilverStripe\Core\Cache\Cache.partials:
  factory: SilverStripe\Core\Cache\CacheFactory
  constructor:
    - "partials"
    - 300

Encourage module developers to do the same.This will mean that the CacheFactory service can be redefined and all default caches will be replaced.

It's your call as to whether the cache factory interface needs a setLifetime as sell

@chillu I amended my example above after looking at how Injector actually works. ;-) Principle is the same

Oh the other thing you could do is have a generic cache factory that lets you define the argument order:

SilverStripe\Core\Cache\CacheFactory:
  class: 'SilverStripe\Core\Cache\DefaultCacheFactory'
  properties:
    Class: 'Symfony\Component\Cache\Simple\FilesystemCache'
    Constructor:
     - '$namespace'
     - '$lifetime'
     - `TEMP_FOLDER`

Or

SilverStripe\Core\Cache\CacheFactory:
  class: 'SilverStripe\Core\Cache\DefaultCacheFactory'
  properties:
    Class: 'Symfony\Component\Cache\Simple\ MemcachedCache'
    Constructor:
     - '%$MemcachedCacheService
     - '$namespace'
     - '$lifetime'

With code something like this:

class DefaultCacheFactory implements CacheFactory {
  public $Class, $Constructor;

  function create($namespace, $lifetime) {
    $mappings = [ '$namespace' => $namespace, '$lifetime' => $lifetime ];
    foreach($this->Constructor as $arg) {
      $mappedArgs[] = isset($mappings[$arg]) ? $mappings[$arg] : $ar;g
    }
    return Injector::inst()->create($this->Class, $mappedArgs);
  }
Was this page helpful?
0 / 5 - 0 ratings