diff --git a/composer.json b/composer.json index c6ce92f6f..45ae114d2 100644 --- a/composer.json +++ b/composer.json @@ -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": "*", diff --git a/src/Core/Cache/DefaultCacheFactory.php b/src/Core/Cache/DefaultCacheFactory.php index b60ecfeb8..856eb769a 100644 --- a/src/Core/Cache/DefaultCacheFactory.php +++ b/src/Core/Cache/DefaultCacheFactory.php @@ -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: @@ -55,6 +56,7 @@ class DefaultCacheFactory implements CacheFactory $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. @@ -66,11 +68,11 @@ class DefaultCacheFactory implements CacheFactory // 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; } @@ -79,9 +81,10 @@ class DefaultCacheFactory implements CacheFactory // 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); } /** @@ -114,20 +117,59 @@ class DefaultCacheFactory implements CacheFactory } /** - * @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; } } diff --git a/src/Core/Cache/FilesystemCacheFactory.php b/src/Core/Cache/FilesystemCacheFactory.php index 628c783f3..822f582f5 100644 --- a/src/Core/Cache/FilesystemCacheFactory.php +++ b/src/Core/Cache/FilesystemCacheFactory.php @@ -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 */ @@ -26,10 +26,11 @@ class FilesystemCacheFactory implements CacheFactory */ 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]); } } diff --git a/src/Core/Cache/ManifestCacheFactory.php b/src/Core/Cache/ManifestCacheFactory.php index b1ceb92b6..52d3b294f 100644 --- a/src/Core/Cache/ManifestCacheFactory.php +++ b/src/Core/Cache/ManifestCacheFactory.php @@ -6,10 +6,9 @@ use BadMethodCallException; 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; @@ -45,6 +44,8 @@ class ManifestCacheFactory extends DefaultCacheFactory { // Override default cache generation with SS_MANIFESTCACHE $cacheClass = Environment::getEnv('SS_MANIFESTCACHE'); + $params['useInjector'] = false; + if (!$cacheClass) { return parent::create($service, $params); } @@ -56,37 +57,18 @@ class ManifestCacheFactory extends DefaultCacheFactory 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; - } } diff --git a/src/i18n/Messages/Symfony/FlushInvalidatedResource.php b/src/i18n/Messages/Symfony/FlushInvalidatedResource.php index 95de3bb19..0fc066f5c 100644 --- a/src/i18n/Messages/Symfony/FlushInvalidatedResource.php +++ b/src/i18n/Messages/Symfony/FlushInvalidatedResource.php @@ -27,7 +27,7 @@ class FlushInvalidatedResource implements SelfCheckingResourceInterface, \Serial return null; } - public function isFresh($timestamp) + public function isFresh(int $timestamp): bool { // Check mtime of canary $canary = static::canary(); diff --git a/tests/php/Core/Cache/RateLimiterTest.php b/tests/php/Core/Cache/RateLimiterTest.php index 011020067..aaf83ead2 100644 --- a/tests/php/Core/Cache/RateLimiterTest.php +++ b/tests/php/Core/Cache/RateLimiterTest.php @@ -6,7 +6,8 @@ use Psr\SimpleCache\CacheInterface; 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 { @@ -19,7 +20,7 @@ class RateLimiterTest extends SapphireTest public function testConstruct() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 5, @@ -33,7 +34,7 @@ class RateLimiterTest extends SapphireTest public function testGetNumberOfAttempts() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 5, @@ -48,7 +49,7 @@ class RateLimiterTest extends SapphireTest public function testGetNumAttemptsRemaining() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 1, @@ -64,7 +65,7 @@ class RateLimiterTest extends SapphireTest public function testGetTimeToReset() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 1, @@ -80,7 +81,7 @@ class RateLimiterTest extends SapphireTest public function testClearAttempts() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 1, @@ -97,7 +98,7 @@ class RateLimiterTest extends SapphireTest public function testHit() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 1, @@ -113,7 +114,7 @@ class RateLimiterTest extends SapphireTest public function testCanAccess() { - $cache = new ArrayCache(); + $cache = $this->getCache(); $rateLimiter = new RateLimiter( 'test', 1, @@ -124,4 +125,9 @@ class RateLimiterTest extends SapphireTest $rateLimiter->hit(); $this->assertFalse($rateLimiter->canAccess()); } + + private function getCache() + { + return new Psr16Cache(new ArrayAdapter()); + } } diff --git a/tests/php/Core/Manifest/VersionProviderTest.php b/tests/php/Core/Manifest/VersionProviderTest.php index 0c5d4e010..21f07b10f 100644 --- a/tests/php/Core/Manifest/VersionProviderTest.php +++ b/tests/php/Core/Manifest/VersionProviderTest.php @@ -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; @@ -15,6 +16,12 @@ class VersionProviderTest extends SapphireTest */ protected $provider; + protected function setup(): void + { + parent::setup(); + $this->clearCache(); + } + public function getMockProvider($composerLockPath = '') { if ($composerLockPath == '') { @@ -95,6 +102,8 @@ class VersionProviderTest extends SapphireTest $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', @@ -145,6 +154,8 @@ class VersionProviderTest extends SapphireTest $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', @@ -202,4 +213,10 @@ class VersionProviderTest extends SapphireTest $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(); + } } diff --git a/tests/php/View/SSViewerCacheBlockTest.php b/tests/php/View/SSViewerCacheBlockTest.php index 79a2674b4..09f334884 100644 --- a/tests/php/View/SSViewerCacheBlockTest.php +++ b/tests/php/View/SSViewerCacheBlockTest.php @@ -11,8 +11,9 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Control\Director; use SilverStripe\View\SSTemplateParseException; use SilverStripe\View\SSViewer; -use Symfony\Component\Cache\Simple\FilesystemCache; -use Symfony\Component\Cache\Simple\NullCache; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Cache\Psr16Cache; // Not actually a data object, we just want a ViewableData object that's just for us @@ -44,9 +45,10 @@ class SSViewerCacheBlockTest extends SapphireTest $cache = null; if ($cacheOn) { - $cache = new FilesystemCache('cacheblock', 0, TempFolder::getTempFolder(BASE_PATH)); // cache indefinitely + // cache indefinitely + $cache = new Psr16Cache(new FilesystemAdapter('cacheblock', 0, TempFolder::getTempFolder(BASE_PATH))); } else { - $cache = new NullCache(); + $cache = new Psr16Cache(new NullAdapter()); } Injector::inst()->registerService($cache, CacheInterface::class . '.cacheblock'); @@ -151,6 +153,7 @@ class SSViewerCacheBlockTest extends SapphireTest $this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', ['Foo' => 1]), '1'); // Test without flush + Injector::inst()->get(Kernel::class)->boot(); Director::test('/'); $this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', ['Foo' => 3]), '1');