Skip to content

Commit

Permalink
Merge pull request #10478 from creative-commoners/pulls/5/symfony6-cache
Browse files Browse the repository at this point in the history
API Update caching to use symfony 6
  • Loading branch information
GuySartorelli authored Sep 7, 2022
2 parents 06b13e0 + c9bc014 commit 8fe2a78
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 65 deletions.
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
"silverstripe/vendor-plugin": "^2",
"sminnee/callbacklist": "^0.1.1",
"swiftmailer/swiftmailer": "^6.3.0",
"symfony/cache": "^4.4.44",
"symfony/config": "^4.4.44",
"symfony/filesystem": "^5.4 || ^6.0",
"symfony/cache": "^6.1",
"symfony/config": "^6.1",
"symfony/filesystem": "^6.1",
"symfony/translation": "^4.4.44",
"symfony/yaml": "^4.4.44",
"symfony/yaml": "^6.1",
"ext-ctype": "*",
"ext-dom": "*",
"ext-hash": "*",
Expand Down
76 changes: 59 additions & 17 deletions src/Core/Cache/DefaultCacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

namespace SilverStripe\Core\Cache;

use InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use Symfony\Component\Cache\Simple\FilesystemCache;
use Symfony\Component\Cache\Simple\ApcuCache;
use Symfony\Component\Cache\Simple\ChainCache;
use Symfony\Component\Cache\Simple\PhpFilesCache;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
use Symfony\Component\Cache\Psr16Cache;

/**
* Returns the most performant combination of caches available on the system:
Expand Down Expand Up @@ -55,6 +56,7 @@ public function create($service, array $args = [])
$defaultLifetime = isset($args['defaultLifetime']) ? $args['defaultLifetime'] : 0;
$directory = isset($args['directory']) ? $args['directory'] : null;
$version = isset($args['version']) ? $args['version'] : null;
$useInjector = isset($args['useInjector']) ? $args['useInjector'] : true;

// In-memory caches are typically more resource constrained (number of items and storage space).
// Give cache consumers an opt-out if they are expecting to create large caches with long lifetimes.
Expand All @@ -66,11 +68,11 @@ public function create($service, array $args = [])

// If apcu isn't supported, phpfiles is the next best preference
if (!$apcuSupported && $phpFilesSupported) {
return $this->createCache(PhpFilesCache::class, [$namespace, $defaultLifetime, $directory]);
return $this->createCache(PhpFilesAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector);
}

// Create filesystem cache
$fs = $this->createCache(FilesystemCache::class, [$namespace, $defaultLifetime, $directory]);
$fs = $this->createCache(FilesystemAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector);
if (!$apcuSupported) {
return $fs;
}
Expand All @@ -79,9 +81,10 @@ public function create($service, array $args = [])
// Note that the cache lifetime will be shorter there by default, to ensure there's enough
// resources for "hot cache" items in APCu as a resource constrained in memory cache.
$apcuNamespace = $namespace . ($namespace ? '_' : '') . md5(BASE_PATH);
$apcu = $this->createCache(ApcuCache::class, [$apcuNamespace, (int) $defaultLifetime / 5, $version]);
$lifetime = (int) $defaultLifetime / 5;
$apcu = $this->createCache(ApcuAdapter::class, [$apcuNamespace, $lifetime, $version], $useInjector);

return $this->createCache(ChainCache::class, [[$apcu, $fs]]);
return $this->createCache(ChainAdapter::class, [[$apcu, $fs]], $useInjector);
}

/**
Expand Down Expand Up @@ -114,20 +117,59 @@ protected function isPHPFilesSupported()
}

/**
* @param string $class
* @param array $args
* @return CacheInterface
* Creates an object with a PSR-16 interface, usually from a PSR-6 class name
*
* Quick explanation of caching standards:
* - Symfony cache implements the PSR-6 standard
* - Symfony provides adapters which wrap a PSR-6 backend with a PSR-16 interface
* - Silverstripe uses the PSR-16 interface to interact with caches. It does not directly interact with the PSR-6 classes
* - Psr\SimpleCache\CacheInterface is the php interface of the PSR-16 standard. All concrete cache classes Silverstripe code interacts with should implement this interface
*
* Further reading:
* - https://symfony.com/doc/current/components/cache/psr6_psr16_adapters.html#using-a-psr-6-cache-object-as-a-psr-16-cache
* - https://github.com/php-fig/simple-cache
*/
protected function createCache($class, $args)
protected function createCache(string $class, array $args, bool $useInjector = true): CacheInterface
{
/** @var CacheInterface $cache */
$cache = Injector::inst()->createWithArgs($class, $args);
$loggerAdded = false;
$classIsPsr6 = is_a($class, CacheItemPoolInterface::class, true);
$classIsPsr16 = is_a($class, CacheInterface::class, true);
if (!$classIsPsr6 && !$classIsPsr16) {
throw new InvalidArgumentException("class $class must implement one of " . CacheItemPoolInterface::class . ' or ' . CacheInterface::class);
}
if ($classIsPsr6) {
$psr6Cache = $this->instantiateCache($class, $args, $useInjector);
$loggerAdded = $this->addLogger($psr6Cache, $loggerAdded);
// Wrap the PSR-6 class inside a class with a PSR-16 interface
$psr16Cache = $this->instantiateCache(Psr16Cache::class, [$psr6Cache], $useInjector);
} else {
$psr16Cache = $this->instantiateCache($class, $args, $useInjector);
}
if (!$loggerAdded) {
$this->addLogger($psr16Cache, $loggerAdded);
}
return $psr16Cache;
}

// Assign cache logger
private function instantiateCache(
string $class,
array $args,
bool $useInjector
): CacheItemPoolInterface|CacheInterface {
if ($useInjector) {
// Injector is used for in most instances to allow modification of the cache implementations
return Injector::inst()->createWithArgs($class, $args);
}
// ManifestCacheFactory cannot use Injector because config is not available at that point
return new $class(...$args);
}

private function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool
{
if ($this->logger && $cache instanceof LoggerAwareInterface) {
$cache->setLogger($this->logger);
return true;
}

return $cache;
return false;
}
}
7 changes: 4 additions & 3 deletions src/Core/Cache/FilesystemCacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
namespace SilverStripe\Core\Cache;

use SilverStripe\Core\Injector\Injector;
use Symfony\Component\Cache\Simple\FilesystemCache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;

class FilesystemCacheFactory implements CacheFactory
{

/**
* @var string Absolute directory path
*/
Expand All @@ -26,10 +26,11 @@ public function __construct($directory)
*/
public function create($service, array $params = [])
{
return Injector::inst()->create(FilesystemCache::class, false, [
$psr6Cache = Injector::inst()->createWithArgs(FilesystemAdapter::class, [
(isset($params['namespace'])) ? $params['namespace'] : '',
(isset($params['defaultLifetime'])) ? $params['defaultLifetime'] : 0,
$this->directory
]);
return Injector::inst()->createWithArgs(Psr16Cache::class, [$psr6Cache]);
}
}
36 changes: 9 additions & 27 deletions src/Core/Cache/ManifestCacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerAwareInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use SilverStripe\Control\Director;
use SilverStripe\Core\Environment;

Expand Down Expand Up @@ -45,6 +44,8 @@ public function create($service, array $params = [])
{
// Override default cache generation with SS_MANIFESTCACHE
$cacheClass = Environment::getEnv('SS_MANIFESTCACHE');
$params['useInjector'] = false;

if (!$cacheClass) {
return parent::create($service, $params);
}
Expand All @@ -56,37 +57,18 @@ public function create($service, array $params = [])
return $factory->create($service, $params);
}

// Check if SS_MANIFESTCACHE is a cache subclass
if (is_a($cacheClass, CacheInterface::class, true)) {
// Check if SS_MANIFESTCACHE is a PSR-6 or PSR-16 class
if (is_a($cacheClass, CacheItemPoolInterface::class, true) ||
is_a($cacheClass, CacheInterface::class, true)
) {
$args = array_merge($this->args, $params);
$namespace = isset($args['namespace']) ? $args['namespace'] : '';
return $this->createCache($cacheClass, [$namespace]);
return $this->createCache($cacheClass, [$namespace], false);
}

// Validate type
throw new BadMethodCallException(
'SS_MANIFESTCACHE is not a valid CacheInterface or CacheFactory class name'
'SS_MANIFESTCACHE is not a valid CacheInterface, CacheItemPoolInterface or CacheFactory class name'
);
}

/**
* Create cache directly without config / injector
*
* @param string $class
* @param array $args
* @return CacheInterface
*/
public function createCache($class, $args)
{
/** @var CacheInterface $cache */
$reflection = new ReflectionClass($class);
$cache = $reflection->newInstanceArgs($args);

// Assign cache logger
if ($this->logger && $cache instanceof LoggerAwareInterface) {
$cache->setLogger($this->logger);
}

return $cache;
}
}
2 changes: 1 addition & 1 deletion src/i18n/Messages/Symfony/FlushInvalidatedResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function getResource()
return null;
}

public function isFresh($timestamp)
public function isFresh(int $timestamp): bool
{
// Check mtime of canary
$canary = static::canary();
Expand Down
22 changes: 14 additions & 8 deletions tests/php/Core/Cache/RateLimiterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
use SilverStripe\Core\Cache\RateLimiter;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use Symfony\Component\Cache\Simple\ArrayCache;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;

class RateLimiterTest extends SapphireTest
{
Expand All @@ -19,7 +20,7 @@ protected function setUp(): void

public function testConstruct()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
5,
Expand All @@ -33,7 +34,7 @@ public function testConstruct()

public function testGetNumberOfAttempts()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
5,
Expand All @@ -48,7 +49,7 @@ public function testGetNumberOfAttempts()

public function testGetNumAttemptsRemaining()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
1,
Expand All @@ -64,7 +65,7 @@ public function testGetNumAttemptsRemaining()

public function testGetTimeToReset()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
1,
Expand All @@ -80,7 +81,7 @@ public function testGetTimeToReset()

public function testClearAttempts()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
1,
Expand All @@ -97,7 +98,7 @@ public function testClearAttempts()

public function testHit()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
1,
Expand All @@ -113,7 +114,7 @@ public function testHit()

public function testCanAccess()
{
$cache = new ArrayCache();
$cache = $this->getCache();
$rateLimiter = new RateLimiter(
'test',
1,
Expand All @@ -124,4 +125,9 @@ public function testCanAccess()
$rateLimiter->hit();
$this->assertFalse($rateLimiter->canAccess());
}

private function getCache()
{
return new Psr16Cache(new ArrayAdapter());
}
}
19 changes: 18 additions & 1 deletion tests/php/Core/Manifest/VersionProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace SilverStripe\Core\Tests\Manifest;

use SebastianBergmann\Version;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\VersionProvider;
use SilverStripe\Dev\SapphireTest;

Expand All @@ -15,6 +16,12 @@ class VersionProviderTest extends SapphireTest
*/
protected $provider;

protected function setup(): void
{
parent::setup();
$this->clearCache();
}

public function getMockProvider($composerLockPath = '')
{
if ($composerLockPath == '') {
Expand Down Expand Up @@ -95,6 +102,8 @@ public function testGetVersionNoRecipe()
$result = $provider->getVersion();
$this->assertStringContainsString('Framework: 1.2.3', $result);

$this->clearCache();

Config::modify()->set(VersionProvider::class, 'modules', [
'silverstripe/framework' => 'Framework',
'silverstripe/recipe-core' => 'Core Recipe',
Expand Down Expand Up @@ -145,6 +154,8 @@ public function testGetVersionRecipeCmsCore()
$this->assertStringContainsString('CMS Recipe: 8.8.8', $result);
$this->assertStringNotContainsString('CWP: 9.9.9', $result);

$this->clearCache();

Config::modify()->set(VersionProvider::class, 'modules', [
'silverstripe/framework' => 'Framework',
'silverstripe/recipe-core' => 'Core Recipe',
Expand Down Expand Up @@ -202,4 +213,10 @@ public function testGetModuleVersion()
$this->assertStringNotContainsString('Framework: 1.2.3', $result);
$this->assertStringContainsString('Core Recipe: 7.7.7', $result);
}

private function clearCache()
{
$cache = Injector::inst()->get(CacheInterface::class . '.VersionProvider');
$cache->clear();
}
}
Loading

0 comments on commit 8fe2a78

Please sign in to comment.