diff --git a/_config.php b/_config.php index a05ee9abb..5547a6829 100644 --- a/_config.php +++ b/_config.php @@ -1,6 +1,6 @@ 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'); diff --git a/_config/cache.yml b/_config/cache.yml new file mode 100644 index 000000000..29ed1e044 --- /dev/null +++ b/_config/cache.yml @@ -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" diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 66baed982..ec4ea4a42 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -17,7 +17,7 @@ use SilverStripe\Control\Controller; 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; @@ -2020,9 +2020,9 @@ class LeftAndMain extends Controller implements PermissionProvider // 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 { @@ -2038,7 +2038,7 @@ class LeftAndMain extends Controller implements PermissionProvider $versions[$package->name] = $package->version; } } - $cache->save(json_encode($versions), $cacheKey); + $cache->set($cacheKey, json_encode($versions)); } } } diff --git a/composer.json b/composer.json index 79160eb4b..ab7990283 100644 --- a/composer.json +++ b/composer.json @@ -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" diff --git a/docs/en/02_Developer_Guides/01_Templates/07_Caching.md b/docs/en/02_Developer_Guides/01_Templates/07_Caching.md index c0d2c51af..aae856118 100644 --- a/docs/en/02_Developer_Guides/01_Templates/07_Caching.md +++ b/docs/en/02_Developer_Guides/01_Templates/07_Caching.md @@ -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 %> diff --git a/docs/en/02_Developer_Guides/08_Performance/01_Caching.md b/docs/en/02_Developer_Guides/08_Performance/01_Caching.md index 98049ac41..596284e60 100644 --- a/docs/en/02_Developer_Guides/08_Performance/01_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/01_Caching.md @@ -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: @@ -21,136 +21,170 @@ 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 (see "Adapters" below for details). +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. + + +## Usage + +Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class interface. :::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; + use Psr\SimpleCache\CacheInterface + $cache = Injector::inst()->get(CacheInterface::class . '.myCache'); -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. + // 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 - :::php - // Disables all caches - Cache::set_cache_lifetime('any', -1, 100); +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): -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. + :::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 - $cache = Cache::factory('foo'); - $cache->clean(Zend_Cache::CLEANING_MODE_ALL); + :::php + use Psr\SimpleCache\CacheInterface + $cache = Injector::inst()->get(CacheInterface::class . '.myCache'); + + // remove the cache item + $cache->delete('myCacheKey'); -A single element can be invalidated through its cache key. +Individual cache items can define a lifetime, after which the cached value is marked as expired: - :::php - $cache = Cache::factory('foo'); - $cache->remove($cachekey); + :::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. +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). - :::php - // set all caches to 3 hours - Cache::set_cache_lifetime('any', 60*60*3); + :::yml + SilverStripe\Core\Injector\Injector: + Psr\SimpleCache\CacheInterface.cacheblock: + constructor: + defaultLifetime: 3600 -## Alternative Cache Backends +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. -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. + :::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')); -## Cleaning caches on flush=1 requests +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. -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. +## Adapters -See [reference documentation on Flushable](/developer_guides/execution_pipeline/flushable/) for implementation details. +SilverStripe tries to identify the most performant cache available on your system +through the [DefaultCacheFactory](api:SilverStripe\Core\Cache\DefaultCacheFactory) implementation: -### Memcached + * - `PhpFilesCache` (PHP 5.6 or PHP 7 with [opcache](http://php.net/manual/en/book.opcache.php) enabled). + This cache has relatively low [memory defaults](http://php.net/manual/en/opcache.configuration.php#ini.opcache.memory-consumption). + We recommend increasing it for large applications, or enabling the + [file_cache fallback](http://php.net/manual/en/opcache.configuration.php#ini.opcache.file-cache) + * - `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. -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. +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`. - :::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); +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. -### APC + :::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 -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). +## Additional Caches - :::php - Cache::add_backend('primary_apc', 'APC'); - Cache::pick_backend('primary_apc', 'any', 10); +Unfortunately not all caches are configurable via cache adapters. -### 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); + * [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) diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 921f3d3da..be56bca02 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -16,6 +16,7 @@ guide developers in preparing existing 3.x code for compatibility with 4.0 * [Filesystem API](#overview-filesystem) * [Template and Form API](#overview-template) * [i18n](#overview-i18n) + * [Cache](#overview-cache) * [Email and Mailer](#overview-mailer) * [Commit History](#commit-history) @@ -49,6 +50,7 @@ guide developers in preparing existing 3.x code for compatibility with 4.0 * Themes are now configured to cascade, where you can specify a list of themes, and have the template engine search programatically through a prioritised list when resolving template and CSS file paths. * i18n Updated to use symfony/translation over zend Framework 1. Zend_Translate has been removed. +* Replaced `Zend_Cache` and the `Cache` API with a PSR-16 implementation (symfony/cache) * _ss_environment.php files have been removed in favour of `.env` and "real" environment variables. ## Upgrading @@ -1473,6 +1475,93 @@ New `TimeField` methods replace `getConfig()` / `setConfig()` * `i18n::get_common_locales()` removed. * `i18n.common_locales` config removed +### Cache API + +We have replaced the unsupported `Zend_Cache` library with [symfony/cache](https://github.com/symfony/cache). +This also allowed us to remove SilverStripe's `Cache` API and use dependency injection with a standard +[PSR-16](http://www.php-fig.org/psr/psr-16/) cache interface instead. + +#### Usage Changes + +Caches should be retrieved through `Injector` instead of `Cache::factory()`, +and have a slightly different API (e.g. `set()` instead of `save()`). + +Before: + + :::php + $cache = Cache::factory('myCache'); + + // create a new item by trying to get it from the cache + $myValue = $cache->load('myCacheKey'); + + // set a value and save it via the adapter + $cache->save(1234, 'myCacheKey'); + + // retrieve the cache item + if (!$cache->load('myCacheKey')) { + // ... item does not exists in the cache + } + + // Remove a cache key + $cache->remove('myCacheKey'); + + +After: + + :::php + 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 + } + + $cache->delete('myCacheKey'); + +#### Configuration Changes + +Caches are now configured through dependency injection services instead of PHP. +See our ["Caching" docs](/developer-guides/performance/caching) for more details. + +Before (`mysite/_config.php`): + + :::php + Cache::add_backend( + 'primary_memcached', + 'Memcached', + array( + 'servers' => array( + 'host' => 'localhost', + 'port' => 11211, + ) + ) + ); + Cache::pick_backend('primary_memcached', 'any', 10); + +After (`mysite/_config/config.yml`): + + :::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 + ### Email and Mailer #### Email Additions / Changes diff --git a/src/Assets/GDBackend.php b/src/Assets/GDBackend.php index b8b4e87f0..a530af2b4 100644 --- a/src/Assets/GDBackend.php +++ b/src/Assets/GDBackend.php @@ -4,12 +4,11 @@ namespace SilverStripe\Assets; use SilverStripe\Assets\Storage\AssetContainer; use SilverStripe\Assets\Storage\AssetStore; -use SilverStripe\Core\Cache; +use Psr\SimpleCache\CacheInterface; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Object; use SilverStripe\Core\Flushable; use InvalidArgumentException; -use Zend_Cache; -use Zend_Cache_Core; /** * A wrapper class for GD-based images, with lots of manipulation functions. @@ -25,7 +24,7 @@ class GDBackend extends Object implements Image_Backend, Flushable protected $gd; /** - * @var Zend_Cache_Core + * @var \Psr\SimpleCache\CacheInterface */ protected $cache; @@ -66,7 +65,7 @@ class GDBackend extends Object implements Image_Backend, Flushable public function __construct(AssetContainer $assetContainer = null) { parent::__construct(); - $this->cache = Cache::factory('GDBackend_Manipulations'); + $this->cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations'); if ($assetContainer) { $this->loadFromContainer($assetContainer); @@ -219,7 +218,7 @@ class GDBackend extends Object implements Image_Backend, Flushable public function failedResample($arg = null) { $key = sha1(implode('|', func_get_args())); - return (bool)$this->cache->load($key); + return (bool)$this->cache->get($key); } /** @@ -259,7 +258,7 @@ class GDBackend extends Object implements Image_Backend, Flushable protected function markFailed($arg = null) { $key = sha1(implode('|', func_get_args())); - $this->cache->save('1', $key); + $this->cache->set($key, '1'); } /** @@ -270,7 +269,7 @@ class GDBackend extends Object implements Image_Backend, Flushable protected function markSucceeded($arg = null) { $key = sha1(implode('|', func_get_args())); - $this->cache->save('0', $key); + $this->cache->set($key, '0'); } @@ -762,8 +761,7 @@ class GDBackend extends Object implements Image_Backend, Flushable public static function flush() { - // Clear factory - $cache = Cache::factory('GDBackend_Manipulations'); - $cache->clean(Zend_Cache::CLEANING_MODE_ALL); + $cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations'); + $cache->clear(); } } diff --git a/src/Core/Cache.php b/src/Core/Cache.php deleted file mode 100644 index 53089e66e..000000000 --- a/src/Core/Cache.php +++ /dev/null @@ -1,201 +0,0 @@ - $cachedir - ) - ); - - self::$cache_lifetime['default'] = array( - 'lifetime' => 600, - 'priority' => 1 - ); - } - } - - /** - * Add a new named cache backend. - * - * @see http://framework.zend.com/manual/en/zend.cache.html - * - * @param string $name The name of this backend as a freeform string - * @param string $type The Zend_Cache backend ('File' or 'Sqlite' or ...) - * @param array $options The Zend_Cache backend options - */ - public static function add_backend($name, $type, $options = array()) - { - self::init(); - self::$backends[$name] = array($type, $options); - } - - /** - * Pick a named cache backend for a particular named cache. - * - * The priority call with the highest number will be the actual backend - * picked. A backend picked for a specific cache name will always be used - * instead of 'any' if it exists, no matter the priority. - * - * @param string $name The name of the backend, as passed as the first argument to add_backend - * @param string $for The name of the cache to pick this backend for (or 'any' for any backend) - * @param integer $priority The priority of this pick - */ - public static function pick_backend($name, $for, $priority = 1) - { - self::init(); - - $current = -1; - - if (isset(self::$backend_picks[$for])) { - $current = self::$backend_picks[$for]['priority']; - } - - if ($priority >= $current) { - self::$backend_picks[$for] = array( - 'name' => $name, - 'priority' => $priority - ); - } - } - - /** - * Return the cache lifetime for a particular named cache. - * - * @param string $for - * - * @return string - */ - public static function get_cache_lifetime($for) - { - if (isset(self::$cache_lifetime[$for])) { - return self::$cache_lifetime[$for]; - } - - return null; - } - - /** - * Set the cache lifetime for a particular named cache - * - * @param string $for The name of the cache to set this lifetime for (or 'any' for all backends) - * @param integer $lifetime The lifetime of an item of the cache, in seconds, or -1 to disable caching - * @param integer $priority The priority. The highest priority setting is used. Unlike backends, 'any' is not - * special in terms of priority. - */ - public static function set_cache_lifetime($for, $lifetime = 600, $priority = 1) - { - self::init(); - - $current = -1; - - if (isset(self::$cache_lifetime[$for])) { - $current = self::$cache_lifetime[$for]['priority']; - } - - if ($priority >= $current) { - self::$cache_lifetime[$for] = array( - 'lifetime' => $lifetime, - 'priority' => $priority - ); - } - } - - /** - * Build a cache object. - * - * @see http://framework.zend.com/manual/en/zend.cache.html - * - * @param string $for The name of the cache to build - * @param string $frontend (optional) The type of Zend_Cache frontend - * @param array $frontendOptions (optional) Any frontend options to use. - * @return Zend_Cache_Core The cache object - */ - public static function factory($for, $frontend = 'Output', $frontendOptions = null) - { - self::init(); - - $backend_name = 'default'; - $backend_priority = -1; - $cache_lifetime = self::$cache_lifetime['default']['lifetime']; - $lifetime_priority = -1; - - foreach (array('any', $for) as $name) { - if (isset(self::$backend_picks[$name])) { - if (self::$backend_picks[$name]['priority'] > $backend_priority) { - $backend_name = self::$backend_picks[$name]['name']; - $backend_priority = self::$backend_picks[$name]['priority']; - } - } - - if (isset(self::$cache_lifetime[$name])) { - if (self::$cache_lifetime[$name]['priority'] > $lifetime_priority) { - $cache_lifetime = self::$cache_lifetime[$name]['lifetime']; - $lifetime_priority = self::$cache_lifetime[$name]['priority']; - } - } - } - - $backend = self::$backends[$backend_name]; - - $basicOptions = array('cache_id_prefix' => $for); - - if ($cache_lifetime >= 0) { - $basicOptions['lifetime'] = $cache_lifetime; - } else { - $basicOptions['caching'] = false; - } - - $frontendOptions = $frontendOptions ? array_merge($basicOptions, $frontendOptions) : $basicOptions; - - require_once 'Zend/Cache.php'; - - return Zend_Cache::factory( - $frontend, - $backend[0], - $frontendOptions, - $backend[1] - ); - } -} diff --git a/src/Core/Cache/ApcuCacheFactory.php b/src/Core/Cache/ApcuCacheFactory.php new file mode 100644 index 000000000..f697b67d8 --- /dev/null +++ b/src/Core/Cache/ApcuCacheFactory.php @@ -0,0 +1,36 @@ +version = $version; + } + + /** + * @inheritdoc + */ + public function create($service, array $params = array()) + { + return Injector::inst()->create(ApcuCache::class, false, [ + (isset($args['namespace'])) ? $args['namespace'] : '', + (isset($args['defaultLifetime'])) ? $args['defaultLifetime'] : 0, + $this->version + ]); + } +} diff --git a/src/Core/Cache/CacheFactory.php b/src/Core/Cache/CacheFactory.php new file mode 100644 index 000000000..1f216c1b0 --- /dev/null +++ b/src/Core/Cache/CacheFactory.php @@ -0,0 +1,20 @@ +get() call), + * this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name. + * + * @param string $class + * @param array $args + * @return CacheInterface + */ + public function create($service, array $params = array()); +} diff --git a/src/Core/Cache/DefaultCacheFactory.php b/src/Core/Cache/DefaultCacheFactory.php new file mode 100644 index 000000000..1c200f007 --- /dev/null +++ b/src/Core/Cache/DefaultCacheFactory.php @@ -0,0 +1,79 @@ +directory = $directory; + $this->version = $version; + } + + /** + * @inheritdoc + */ + public function create($service, array $params = array()) + { + $namespace = (isset($args['namespace'])) ? $args['namespace'] : ''; + $defaultLifetime = (isset($args['defaultLifetime'])) ? $args['defaultLifetime'] : 0; + $version = $this->version; + $directory = $this->directory; + + $apcuSupported = null; + $phpFilesSupported = null; + + if (null === $apcuSupported) { + $apcuSupported = ApcuAdapter::isSupported(); + } + + if (!$apcuSupported && null === $phpFilesSupported) { + $phpFilesSupported = PhpFilesAdapter::isSupported(); + } + + if ($phpFilesSupported) { + $opcache = Injector::inst()->create(PhpFilesCache::class, false, [$namespace, $defaultLifetime, $directory]); + return $opcache; + } + + $fs = Injector::inst()->create(FilesystemCache::class, false, [$namespace, $defaultLifetime, $directory]); + if (!$apcuSupported) { + return $fs; + } + + $apcu = Injector::inst()->create(ApcuCache::class, false, [$namespace, (int) $defaultLifetime / 5, $version]); + + return Injector::inst()->create(ChainCache::class, false, [[$apcu, $fs]]); + } +} diff --git a/src/Core/Cache/FilesystemCacheFactory.php b/src/Core/Cache/FilesystemCacheFactory.php new file mode 100644 index 000000000..36094cc6e --- /dev/null +++ b/src/Core/Cache/FilesystemCacheFactory.php @@ -0,0 +1,35 @@ +directory = $directory; + } + + /** + * @inheritdoc + */ + public function create($service, array $params = array()) + { + return Injector::inst()->create(FilesystemCache::class, false, [ + (isset($args['namespace'])) ? $args['namespace'] : '', + (isset($args['defaultLifetime'])) ? $args['defaultLifetime'] : 0, + $this->directory + ]); + } +} diff --git a/src/Core/Cache/MemcachedCacheFactory.php b/src/Core/Cache/MemcachedCacheFactory.php new file mode 100644 index 000000000..37269da02 --- /dev/null +++ b/src/Core/Cache/MemcachedCacheFactory.php @@ -0,0 +1,36 @@ +memcachedClient = $memcachedClient; + } + + /** + * @inheritdoc + */ + public function create($service, array $params = array()) + { + return Injector::inst()->create(MemcachedCache::class, false, [ + $this->memcachedClient, + (isset($args['namespace'])) ? $args['namespace'] : '', + (isset($args['defaultLifetime'])) ? $args['defaultLifetime'] : 0 + ]); + } +} diff --git a/src/Core/Core.php b/src/Core/Core.php index efd99c10f..93105fc8e 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -54,14 +54,6 @@ mb_regex_encoding('UTF-8'); */ gc_enable(); -/** - * Include the Zend autoloader. This will be removed in the near future. - */ -if (file_exists('thirdparty/Zend/Loader/Autoloader.php')) { - require_once 'thirdparty/Zend/Loader/Autoloader.php'; - Zend_Loader_Autoloader::getInstance(); -} - // Initialise the dependency injector as soon as possible, as it is // subsequently used by some of the following code $injector = new Injector(array('locator' => 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator')); diff --git a/src/Core/Manifest/ConfigManifest.php b/src/Core/Manifest/ConfigManifest.php index 48a4f09ab..883577b54 100644 --- a/src/Core/Manifest/ConfigManifest.php +++ b/src/Core/Manifest/ConfigManifest.php @@ -7,10 +7,9 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\DAG; use SilverStripe\Core\Config\DAG_CyclicException; use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Cache; +use Symfony\Component\Cache\Simple\FilesystemCache; use Symfony\Component\Yaml\Parser; use Traversable; -use Zend_Cache_Core; /** * A utility class which builds a manifest of configuration items @@ -28,7 +27,7 @@ class ConfigManifest protected $includeTests; /** - * @var Zend_Cache_Core + * @var FilesystemCache */ protected $cache; @@ -114,15 +113,15 @@ class ConfigManifest $this->key = sha1($base).'_'; $this->includeTests = $includeTests; - // Get the Zend Cache to load/store cache into + // Get a cache singleton $this->cache = $this->getCache(); // Unless we're forcing regen, try loading from cache if (!$forceRegen) { // The PHP config sources are always needed - $this->phpConfigSources = $this->cache->load($this->key.'php_config_sources'); + $this->phpConfigSources = $this->cache->get($this->key.'php_config_sources'); // Get the variant key spec - $this->variantKeySpec = $this->cache->load($this->key.'variant_key_spec'); + $this->variantKeySpec = $this->cache->get($this->key.'variant_key_spec'); } // If we don't have a variantKeySpec (because we're forcing regen, or it just wasn't in the cache), generate it @@ -136,14 +135,12 @@ class ConfigManifest /** * Provides a hook for mock unit tests despite no DI - * @return Zend_Cache_Core + * @return \Psr\SimpleCache\CacheInterface */ protected function getCache() { - return Cache::factory('SS_Configuration', 'Core', array( - 'automatic_serialization' => true, - 'lifetime' => null - )); + // TODO Replace with CoreConfigCreator, see https://github.com/silverstripe/silverstripe-framework/pull/6641/files#diff-f8c9b17e06432278197a7d5c3a1043cb + return new FilesystemCache('SS_Configuration', 0, getTempFolder()); } /** @@ -264,9 +261,9 @@ class ConfigManifest $this->buildVariantKeySpec(); if ($cache) { - $this->cache->save($this->phpConfigSources, $this->key.'php_config_sources'); - $this->cache->save($this->yamlConfigFragments, $this->key.'yaml_config_fragments'); - $this->cache->save($this->variantKeySpec, $this->key.'variant_key_spec'); + $this->cache->set($this->key.'php_config_sources', $this->phpConfigSources); + $this->cache->set($this->key.'yaml_config_fragments', $this->yamlConfigFragments); + $this->cache->set($this->key.'variant_key_spec', $this->variantKeySpec); } } @@ -650,12 +647,12 @@ class ConfigManifest // given variant is stale compared to the complete set of fragments if (!$this->yamlConfigFragments) { // First try and just load the exact variant - if ($this->yamlConfig = $this->cache->load($this->key.'yaml_config_'.$this->variantKey())) { + if ($this->yamlConfig = $this->cache->get($this->key.'yaml_config_'.$this->variantKey())) { $this->yamlConfigVariantKey = $this->variantKey(); return; } // Otherwise try and load the fragments so we can build the variant else { - $this->yamlConfigFragments = $this->cache->load($this->key.'yaml_config_fragments'); + $this->yamlConfigFragments = $this->cache->get($this->key.'yaml_config_fragments'); } } @@ -684,7 +681,7 @@ class ConfigManifest } if ($cache) { - $this->cache->save($this->yamlConfig, $this->key.'yaml_config_'.$this->variantKey()); + $this->cache->set($this->key.'yaml_config_'.$this->variantKey(), $this->yamlConfig); } // Since yamlConfig has changed, call any callbacks that are interested diff --git a/src/View/SSTemplateParser.peg b/src/View/SSTemplateParser.peg index f91edc6a9..5d1240758 100644 --- a/src/View/SSTemplateParser.peg +++ b/src/View/SSTemplateParser.peg @@ -743,10 +743,10 @@ class SSTemplateParser extends Parser implements TemplateParser // Get any condition $condition = isset($res['condition']) ? $res['condition'] : ''; - $res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL; + $res['php'] .= 'if ('.$condition.'($partial = $cache->get('.$key.'))) $val .= $partial;' . PHP_EOL; $res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL; $res['php'] .= $sub['php'] . PHP_EOL; - $res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL; + $res['php'] .= $condition . ' $cache->set('.$key.', $val); $val = $oldval . $val;' . PHP_EOL; $res['php'] .= '}'; } diff --git a/src/View/SSTemplateParser.php b/src/View/SSTemplateParser.php index 360a811c1..f5c2a30ce 100644 --- a/src/View/SSTemplateParser.php +++ b/src/View/SSTemplateParser.php @@ -4020,10 +4020,10 @@ class SSTemplateParser extends Parser implements TemplateParser // Get any condition $condition = isset($res['condition']) ? $res['condition'] : ''; - $res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL; + $res['php'] .= 'if ('.$condition.'($partial = $cache->get('.$key.'))) $val .= $partial;' . PHP_EOL; $res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL; $res['php'] .= $sub['php'] . PHP_EOL; - $res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL; + $res['php'] .= $condition . ' $cache->set('.$key.', $val); $val = $oldval . $val;' . PHP_EOL; $res['php'] .= '}'; } diff --git a/src/View/SSViewer.php b/src/View/SSViewer.php index 151b5452f..6b3cc09e2 100644 --- a/src/View/SSViewer.php +++ b/src/View/SSViewer.php @@ -4,7 +4,7 @@ namespace SilverStripe\View; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\ClassInfo; -use SilverStripe\Core\Cache; +use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Convert; use SilverStripe\Core\Flushable; use SilverStripe\Core\Injector\Injector; @@ -14,9 +14,6 @@ use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\Permission; use InvalidArgumentException; -use Zend_Cache_Backend_ExtendedInterface; -use Zend_Cache; -use Zend_Cache_Core; /** * Parses a template file with an *.ss file extension. @@ -466,17 +463,8 @@ class SSViewer implements Flushable public static function flush_cacheblock_cache($force = false) { if (!self::$cacheblock_cache_flushed || $force) { - $cache = Cache::factory('cacheblock'); - $backend = $cache->getBackend(); - - if ($backend instanceof Zend_Cache_Backend_ExtendedInterface - && ($capabilities = $backend->getCapabilities()) - && $capabilities['tags'] - ) { - $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $cache->getTags()); - } else { - $cache->clean(Zend_Cache::CLEANING_MODE_ALL); - } + $cache = Injector::inst()->get(CacheInterface::class . '.cacheblock'); + $cache->clear(); self::$cacheblock_cache_flushed = true; @@ -484,14 +472,14 @@ class SSViewer implements Flushable } /** - * @var Zend_Cache_Core + * @var CacheInterface */ protected $partialCacheStore = null; /** * Set the cache object to use when storing / retrieving partial cache blocks. * - * @param Zend_Cache_Core $cache + * @param CacheInterface $cache */ public function setPartialCacheStore($cache) { @@ -501,11 +489,11 @@ class SSViewer implements Flushable /** * Get the cache object to use when storing / retrieving partial cache blocks. * - * @return Zend_Cache_Core + * @return CacheInterface */ public function getPartialCacheStore() { - return $this->partialCacheStore ? $this->partialCacheStore : Cache::factory('cacheblock'); + return $this->partialCacheStore ? $this->partialCacheStore : Injector::inst()->get(CacheInterface::class . '.cacheblock'); } /** diff --git a/tests/php/Assets/GDTest.php b/tests/php/Assets/GDTest.php index fa43d6960..127d6f38c 100644 --- a/tests/php/Assets/GDTest.php +++ b/tests/php/Assets/GDTest.php @@ -3,7 +3,8 @@ namespace SilverStripe\Assets\Tests; use SilverStripe\Assets\GDBackend; -use SilverStripe\Core\Cache; +use Psr\SimpleCache\CacheInterface; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; /** @@ -192,9 +193,9 @@ class GDTest extends SapphireTest $gd->loadFrom($fullPath); // Cache should refer to this file - $cache = Cache::factory('GDBackend_Manipulations'); + $cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations'); $key = sha1(implode('|', array($fullPath, filemtime($fullPath)))); - $data = $cache->load($key); + $data = $cache->get($key); $this->assertEquals('1', $data); } diff --git a/tests/php/Core/CacheTest.php b/tests/php/Core/CacheTest.php deleted file mode 100644 index b16978100..000000000 --- a/tests/php/Core/CacheTest.php +++ /dev/null @@ -1,77 +0,0 @@ -save('Good', 'cachekey'); - $this->assertEquals('Good', $cache->load('cachekey')); - } - - public function testCacheCanBeDisabled() - { - Cache::set_cache_lifetime('test', -1, 10); - - $cache = Cache::factory('test'); - - $cache->save('Good', 'cachekey'); - $this->assertFalse($cache->load('cachekey')); - } - - public function testCacheLifetime() - { - Cache::set_cache_lifetime('test', 0.5, 20); - - $cache = Cache::factory('test'); - $this->assertEquals(0.5, $cache->getOption('lifetime')); - - $cache->save('Good', 'cachekey'); - $this->assertEquals('Good', $cache->load('cachekey')); - - // As per documentation, sleep may not sleep for the amount of time you tell it to sleep for - // This loop can make sure it *does* sleep for that long - $endtime = time() + 2; - while (time() < $endtime) { - // Sleep for another 2 seconds! - // This may end up sleeping for 4 seconds, but it's awwwwwwwright. - sleep(2); - } - - $this->assertFalse($cache->load('cachekey')); - } - - public function testCacheSeperation() - { - $cache1 = Cache::factory('test1'); - $cache2 = Cache::factory('test2'); - - $cache1->save('Foo', 'cachekey'); - $cache2->save('Bar', 'cachekey'); - $this->assertEquals('Foo', $cache1->load('cachekey')); - $this->assertEquals('Bar', $cache2->load('cachekey')); - - $cache1->remove('cachekey'); - $this->assertFalse($cache1->load('cachekey')); - $this->assertEquals('Bar', $cache2->load('cachekey')); - } - - public function testCacheDefault() - { - Cache::set_cache_lifetime('default', 1200); - $default = Cache::get_cache_lifetime('default'); - - $this->assertEquals(1200, $default['lifetime']); - - $cache = Cache::factory('somethingnew'); - - $this->assertEquals(1200, $cache->getOption('lifetime')); - } -} diff --git a/tests/php/Core/Manifest/ConfigManifestTest.php b/tests/php/Core/Manifest/ConfigManifestTest.php index e6e7f5d01..2e9592aa1 100644 --- a/tests/php/Core/Manifest/ConfigManifestTest.php +++ b/tests/php/Core/Manifest/ConfigManifestTest.php @@ -7,7 +7,7 @@ use SilverStripe\Core\Config\Config; use SilverStripe\Core\Manifest\ConfigManifest; use SilverStripe\Dev\SapphireTest; use ReflectionProperty; -use Zend_Cache_Core; +use Symfony\Component\Cache\Simple\ArrayCache; class ConfigManifestTest extends SapphireTest { @@ -34,13 +34,13 @@ class ConfigManifestTest extends SapphireTest /** * A helper method to return a mock of the cache in order to test expectations and reduce dependency * - * @return Zend_Cache_Core + * @return \PHPUnit_Framework_MockObject_MockObject */ protected function getCacheMock() { return $this->getMock( - 'Zend_Cache_Core', - array('load', 'save'), + ArrayCache::class, + array('set', 'get'), array(), '', false @@ -51,7 +51,7 @@ class ConfigManifestTest extends SapphireTest * A helper method to return a mock of the manifest in order to test expectations and reduce dependency * * @param $methods - * @return ConfigManifest + * @return \PHPUnit_Framework_MockObject_MockObject */ protected function getManifestMock($methods) { @@ -82,7 +82,7 @@ class ConfigManifestTest extends SapphireTest // Set up a cache where we expect load to never be called $cache = $this->getCacheMock(); $cache->expects($this->never()) - ->method('load'); + ->method('get'); $manifest->expects($this->any()) ->method('getCache') @@ -95,7 +95,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->atLeastOnce()) - ->method('save'); + ->method('set'); $manifest->expects($this->any()) ->method('getCache') @@ -119,7 +119,7 @@ class ConfigManifestTest extends SapphireTest // Load should be called twice $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) - ->method('load'); + ->method('get'); $manifest->expects($this->any()) ->method('getCache') @@ -133,7 +133,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) - ->method('load') + ->method('get') ->will($this->onConsecutiveCalls(false, false)); $manifest->expects($this->any()) @@ -151,7 +151,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) - ->method('load') + ->method('get') ->will($this->onConsecutiveCalls(array(), array())); $manifest->expects($this->any()) @@ -186,7 +186,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) ->will($this->returnValue(false)) - ->method('load'); + ->method('get'); $manifest->expects($this->any()) ->method('getCache') @@ -204,7 +204,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) - ->method('load') + ->method('get') ->will($this->returnCallback(function ($parameter) { if (strpos($parameter, 'variant_key_spec') !== false) { return false; @@ -227,7 +227,7 @@ class ConfigManifestTest extends SapphireTest $cache = $this->getCacheMock(); $cache->expects($this->exactly(2)) - ->method('load') + ->method('get') ->will($this->returnCallback(function ($parameter) { if (strpos($parameter, 'php_config_sources') !== false) { return false; diff --git a/tests/php/ORM/GDImageTest.php b/tests/php/ORM/GDImageTest.php index 8b594ec11..f6169156d 100644 --- a/tests/php/ORM/GDImageTest.php +++ b/tests/php/ORM/GDImageTest.php @@ -5,8 +5,8 @@ namespace SilverStripe\ORM\Tests; require_once __DIR__ . "/ImageTest.php"; use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Cache; -use Zend_Cache; +use Psr\SimpleCache\CacheInterface; +use SilverStripe\Core\Injector\Injector; class GDImageTest extends ImageTest { @@ -32,8 +32,9 @@ class GDImageTest extends ImageTest public function tearDown() { - $cache = Cache::factory('GDBackend_Manipulations'); - $cache->clean(Zend_Cache::CLEANING_MODE_ALL); + $cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations'); + $cache->clear(); + parent::tearDown(); } } diff --git a/tests/php/View/SSViewerCacheBlockTest.php b/tests/php/View/SSViewerCacheBlockTest.php index 8e021f081..7f11325e6 100644 --- a/tests/php/View/SSViewerCacheBlockTest.php +++ b/tests/php/View/SSViewerCacheBlockTest.php @@ -2,11 +2,14 @@ namespace SilverStripe\View\Tests; +use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\Versioning\Versioned; -use SilverStripe\Core\Cache; +use Psr\SimpleCache\CacheInterface; use SilverStripe\Dev\SapphireTest; use SilverStripe\Control\Director; use SilverStripe\View\SSViewer; +use Symfony\Component\Cache\Simple\FilesystemCache; +use Symfony\Component\Cache\Simple\NullCache; // Not actually a data object, we just want a ViewableData object that's just for us @@ -27,8 +30,15 @@ class SSViewerCacheBlockTest extends SapphireTest { $this->data = new SSViewerCacheBlockTest\TestModel(); - Cache::factory('cacheblock')->clean(); - Cache::set_cache_lifetime('cacheblock', $cacheOn ? 600 : -1); + $cache = null; + if ($cacheOn) { + $cache = new FilesystemCache('cacheblock', 0, getTempFolder()); // cache indefinitely + } else { + $cache = new NullCache(); + } + + Injector::inst()->registerService($cache, CacheInterface::class . '.cacheblock'); + Injector::inst()->get(CacheInterface::class . '.cacheblock')->clear(); } protected function _runtemplate($template, $data = null)