mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #11300 from creative-commoners/pulls/6/in-memory-cache
FIX Ensure cache is shared between CLI and webserver
This commit is contained in:
commit
386ff5a1cc
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -12,3 +12,9 @@ jobs:
|
||||
with:
|
||||
# Turn phpcoverage off because it causes a segfault
|
||||
phpcoverage_force_off: true
|
||||
phpunit_skip_suites: framework-cache-only
|
||||
# Ensure we test all in-memory cache factories provided in core
|
||||
extra_jobs: |
|
||||
- phpunit: true
|
||||
phpunit_suite: framework-cache-only
|
||||
install_in_memory_cache_exts: true
|
||||
|
@ -7,7 +7,6 @@ SilverStripe\Core\Injector\Injector:
|
||||
constructor:
|
||||
args:
|
||||
directory: '`TEMP_PATH`'
|
||||
version: null
|
||||
logger: '%$Psr\Log\LoggerInterface'
|
||||
Psr\SimpleCache\CacheInterface.cacheblock:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
|
@ -21,6 +21,10 @@ Requires PHPUnit ^9
|
||||
<testsuite name="cms">
|
||||
<directory>vendor/silverstripe/cms/tests</directory>
|
||||
</testsuite>
|
||||
<!-- Cache suite is separate so it only runs what it needs to run. Intentionally also included in core above. -->
|
||||
<testsuite name="framework-cache-only">
|
||||
<directory>tests/php/Core/Cache</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||
|
@ -12,7 +12,7 @@ use Symfony\Component\Mailer\Transport;
|
||||
*/
|
||||
class TransportFactory implements Factory
|
||||
{
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): object
|
||||
{
|
||||
$dsn = Environment::getEnv('MAILER_DSN') ?: $params['dsn'];
|
||||
$dispatcher = $params['dispatcher'];
|
||||
|
100
src/Core/Cache/AbstractCacheFactory.php
Normal file
100
src/Core/Cache/AbstractCacheFactory.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
|
||||
/**
|
||||
* Abstract implementation of CacheFactory which provides methods to easily instantiate PSR6 and PSR16 cache adapters.
|
||||
*/
|
||||
abstract class AbstractCacheFactory implements CacheFactory
|
||||
{
|
||||
protected ?LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @param LoggerInterface $logger Logger instance to assign
|
||||
*/
|
||||
public function __construct(LoggerInterface $logger = null)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
string $class,
|
||||
array $args,
|
||||
bool $useInjector = true
|
||||
): CacheInterface {
|
||||
$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);
|
||||
}
|
||||
$cacheAdapter = $this->instantiateCache($class, $args, $useInjector);
|
||||
$psr16Cache = $this->prepareCacheForUse($cacheAdapter, $useInjector);
|
||||
return $psr16Cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a cache adapter for use.
|
||||
* This wraps a PSR6 adapter inside a PSR16 one. It also adds the loggers.
|
||||
*/
|
||||
protected function prepareCacheForUse(
|
||||
CacheItemPoolInterface|CacheInterface $cacheAdapter,
|
||||
bool $useInjector
|
||||
): CacheInterface {
|
||||
$loggerAdded = false;
|
||||
if ($cacheAdapter instanceof CacheItemPoolInterface) {
|
||||
$loggerAdded = $this->addLogger($cacheAdapter);
|
||||
// Wrap the PSR-6 class inside a class with a PSR-16 interface
|
||||
$cacheAdapter = $this->instantiateCache(Psr16Cache::class, [$cacheAdapter], $useInjector);
|
||||
}
|
||||
if (!$loggerAdded) {
|
||||
$this->addLogger($cacheAdapter);
|
||||
}
|
||||
return $cacheAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a cache adapter, either via the dependency injector or using the new keyword.
|
||||
*/
|
||||
protected 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);
|
||||
}
|
||||
|
||||
protected function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool
|
||||
{
|
||||
if ($this->logger && ($cache instanceof LoggerAwareInterface)) {
|
||||
$cache->setLogger($this->logger);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -2,39 +2,62 @@
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RuntimeException;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Core\Environment;
|
||||
use Symfony\Component\Cache\Adapter\ApcuAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
|
||||
class ApcuCacheFactory implements CacheFactory
|
||||
/**
|
||||
* Factory to instantiate an ApcuAdapter for use in caching.
|
||||
*
|
||||
* Note that APCu cache may not be shared between your webserver and the CLI.
|
||||
* Flushing the cache from your terminal may not flush the cache used by the webserver.
|
||||
* See https://github.com/symfony/symfony/discussions/54066
|
||||
*/
|
||||
class ApcuCacheFactory extends AbstractCacheFactory implements InMemoryCacheFactory
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
*/
|
||||
public function __construct($version = null)
|
||||
{
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): CacheInterface
|
||||
{
|
||||
$psr6Cache = $this->createPsr6($service, $params);
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
return $this->prepareCacheForUse($psr6Cache, $useInjector);
|
||||
}
|
||||
|
||||
public function createPsr6(string $service, array $params = []): CacheItemPoolInterface
|
||||
{
|
||||
if (!$this->isSupported()) {
|
||||
throw new RuntimeException('APCu is not supported in the current environment. Cannot use APCu cache.');
|
||||
}
|
||||
|
||||
$namespace = isset($params['namespace'])
|
||||
? $params['namespace'] . '_' . md5(BASE_PATH)
|
||||
: md5(BASE_PATH);
|
||||
$defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0;
|
||||
$psr6Cache = Injector::inst()->createWithArgs(ApcuAdapter::class, [
|
||||
$namespace,
|
||||
$defaultLifetime,
|
||||
$this->version
|
||||
]);
|
||||
return Injector::inst()->createWithArgs(Psr16Cache::class, [$psr6Cache]);
|
||||
// $version is optional - defaults to null.
|
||||
$version = isset($params['version']) ? $params['version'] : Environment::getEnv('SS_APCU_VERSION');
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
|
||||
return $this->instantiateCache(
|
||||
ApcuAdapter::class,
|
||||
[$namespace, $defaultLifetime, $version],
|
||||
$useInjector
|
||||
);
|
||||
}
|
||||
|
||||
private function isSupported(): bool
|
||||
{
|
||||
static $isSupported = null;
|
||||
if (null === $isSupported) {
|
||||
// Need to check for CLI because Symfony won't: https://github.com/symfony/symfony/pull/25080
|
||||
$isSupported = Director::is_cli()
|
||||
? filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL) && ApcuAdapter::isSupported()
|
||||
: ApcuAdapter::isSupported();
|
||||
}
|
||||
return $isSupported;
|
||||
}
|
||||
}
|
||||
|
@ -7,14 +7,9 @@ use SilverStripe\Core\Injector\Factory as InjectorFactory;
|
||||
|
||||
interface CacheFactory extends InjectorFactory
|
||||
{
|
||||
|
||||
/**
|
||||
* Note: While the returned object is used as a singleton (by the originating Injector->get() call),
|
||||
* this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name.
|
||||
*
|
||||
* @param string $service
|
||||
* @param array $params
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function create($service, array $params = []);
|
||||
public function create(string $service, array $params = []): CacheInterface;
|
||||
}
|
||||
|
@ -2,38 +2,24 @@
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
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:
|
||||
* - `PhpFilesCache` (PHP 7 with opcache enabled)
|
||||
* - `ApcuCache` (requires APC) with a `FilesystemCache` fallback (for larger cache volumes)
|
||||
* - `FilesystemCache` if none of the above is available
|
||||
*
|
||||
* Modelled after `Symfony\Component\Cache\Adapter\AbstractAdapter::createSystemCache()`
|
||||
* Creates the following cache adapters:
|
||||
* - `PhpFilesAdapter` (falls back to `FilesystemAdapter` if `PhpFilesAdapter` isn't supported)
|
||||
* - An optional in-memory cache such as Redis, Memcached, or APCu.
|
||||
*/
|
||||
class DefaultCacheFactory implements CacheFactory
|
||||
class DefaultCacheFactory extends AbstractCacheFactory
|
||||
{
|
||||
/**
|
||||
* @var string Absolute directory path
|
||||
*/
|
||||
protected $args = [];
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
protected array $args = [];
|
||||
|
||||
/**
|
||||
* @param array $args List of global options to merge with args during create()
|
||||
@ -42,148 +28,98 @@ class DefaultCacheFactory implements CacheFactory
|
||||
public function __construct($args = [], LoggerInterface $logger = null)
|
||||
{
|
||||
$this->args = $args;
|
||||
$this->logger = $logger;
|
||||
parent::__construct($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create($service, array $args = [])
|
||||
public function create(string $service, array $args = []): CacheInterface
|
||||
{
|
||||
// merge args with default
|
||||
$args = array_merge($this->args, $args);
|
||||
$namespace = isset($args['namespace']) ? $args['namespace'] : '';
|
||||
$defaultLifetime = isset($args['defaultLifetime']) ? $args['defaultLifetime'] : 0;
|
||||
$defaultLifetime = (int) (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.
|
||||
$useInMemoryCache = isset($args['useInMemoryCache']) ? $args['useInMemoryCache'] : true;
|
||||
$inMemoryCacheFactory = Environment::getEnv('SS_IN_MEMORY_CACHE_FACTORY');
|
||||
|
||||
// Check support
|
||||
$apcuSupported = ($this->isAPCUSupported() && $useInMemoryCache);
|
||||
$phpFilesSupported = $this->isPHPFilesSupported();
|
||||
|
||||
// If apcu isn't supported, phpfiles is the next best preference
|
||||
if (!$apcuSupported && $phpFilesSupported) {
|
||||
return $this->createCache(PhpFilesAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector);
|
||||
$filesystemCache = $this->instantiateFilesystemCache($namespace, $defaultLifetime, $directory, $useInjector);
|
||||
if (!$useInMemoryCache || !$inMemoryCacheFactory) {
|
||||
return $this->prepareCacheForUse($filesystemCache, $useInjector);
|
||||
}
|
||||
|
||||
// Create filesystem cache
|
||||
if (!$apcuSupported) {
|
||||
return $this->createCache(
|
||||
FilesystemAdapter::class,
|
||||
[$namespace, $defaultLifetime, $directory],
|
||||
$useInjector
|
||||
// Check if SS_IN_MEMORY_CACHE_FACTORY is a factory
|
||||
if (!is_a($inMemoryCacheFactory, InMemoryCacheFactory::class, true)) {
|
||||
throw new LogicException(
|
||||
'The value in your SS_IN_MEMORY_CACHE_FACTORY environment variable'
|
||||
. ' is not a valid InMemoryCacheFactory class name'
|
||||
);
|
||||
}
|
||||
|
||||
// Create PSR6 filesystem + apcu cache's wrapped in a PSR6 chain adapter, then wrap in a PSR16 class
|
||||
$fs = $this->instantiateCache(
|
||||
FilesystemAdapter::class,
|
||||
[$namespace, $defaultLifetime, $directory],
|
||||
// Note that the cache lifetime will be shorter there by default, to ensure there's enough
|
||||
// resources for "hot cache" items as a resource constrained in memory cache.
|
||||
$inMemoryLifetime = (int) ($defaultLifetime / 5);
|
||||
$inMemoryCache = $this->instantiateInMemoryCache(
|
||||
$service,
|
||||
$inMemoryCacheFactory,
|
||||
['namespace' => $namespace, 'defaultLifetime' => $inMemoryLifetime, 'useInjector' => $useInjector],
|
||||
$useInjector
|
||||
);
|
||||
|
||||
// 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);
|
||||
$lifetime = (int) $defaultLifetime / 5;
|
||||
$apcu = $this->instantiateCache(ApcuAdapter::class, [$apcuNamespace, $lifetime, $version], $useInjector);
|
||||
// The ChainAdapter doesn't take a logger, so we need to make sure to add it to the child cache adapters.
|
||||
$this->addLogger($filesystemCache);
|
||||
|
||||
return $this->createCache(ChainAdapter::class, [[$apcu, $fs]], $useInjector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if apcu is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAPCUSupported()
|
||||
{
|
||||
static $apcuSupported = null;
|
||||
if (null === $apcuSupported) {
|
||||
// Need to check for CLI because Symfony won't: https://github.com/symfony/symfony/pull/25080
|
||||
$apcuSupported = Director::is_cli()
|
||||
? filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL) && ApcuAdapter::isSupported()
|
||||
: ApcuAdapter::isSupported();
|
||||
}
|
||||
return $apcuSupported;
|
||||
return $this->createCache(ChainAdapter::class, [[$inMemoryCache, $filesystemCache]], $useInjector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if PHP files is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isPHPFilesSupported()
|
||||
protected function isPHPFilesSupported(): bool
|
||||
{
|
||||
static $phpFilesSupported = null;
|
||||
if (null === $phpFilesSupported) {
|
||||
$phpFilesSupported = PhpFilesAdapter::isSupported();
|
||||
// Only consider to be enabled if opcache is enabled in CLI, or else
|
||||
// filesystem cache won't be shared between webserver and CLI.
|
||||
$phpFilesSupported = PhpFilesAdapter::isSupported() &&
|
||||
filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
return $phpFilesSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Instantiate the cache adapter for the filesystem cache.
|
||||
*/
|
||||
protected function createCache(
|
||||
string $class,
|
||||
array $args,
|
||||
bool $useInjector = true
|
||||
): CacheInterface {
|
||||
$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;
|
||||
}
|
||||
|
||||
private function instantiateCache(
|
||||
string $class,
|
||||
array $args,
|
||||
private function instantiateFilesystemCache(
|
||||
string $namespace,
|
||||
int $defaultLifetime,
|
||||
string $directory,
|
||||
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);
|
||||
): CacheInterface|CacheItemPoolInterface {
|
||||
if ($this->isPHPFilesSupported()) {
|
||||
return $this->instantiateCache(PhpFilesAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector);
|
||||
}
|
||||
// ManifestCacheFactory cannot use Injector because config is not available at that point
|
||||
return new $class(...$args);
|
||||
return $this->instantiateCache(FilesystemAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector);
|
||||
}
|
||||
|
||||
private function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool
|
||||
/**
|
||||
* Instantiate the cache adapter for the in-memory cache.
|
||||
*/
|
||||
private function instantiateInMemoryCache(string $service, string $inMemoryCacheFactory, array $args): CacheItemPoolInterface
|
||||
{
|
||||
if ($this->logger && $cache instanceof LoggerAwareInterface) {
|
||||
$cache->setLogger($this->logger);
|
||||
return true;
|
||||
if ($args['useInjector']) {
|
||||
$factory = Injector::inst()->create($inMemoryCacheFactory);
|
||||
} else {
|
||||
$factory = new $inMemoryCacheFactory();
|
||||
}
|
||||
return false;
|
||||
/** @var InMemoryCacheFactory $factory */
|
||||
$adapter = $factory->createPsr6($service, $args);
|
||||
$this->addLogger($adapter);
|
||||
return $adapter;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
@ -24,7 +25,7 @@ class FilesystemCacheFactory implements CacheFactory
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): CacheInterface
|
||||
{
|
||||
$psr6Cache = Injector::inst()->createWithArgs(FilesystemAdapter::class, [
|
||||
(isset($params['namespace'])) ? $params['namespace'] : '',
|
||||
|
17
src/Core/Cache/InMemoryCacheFactory.php
Normal file
17
src/Core/Cache/InMemoryCacheFactory.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by all cache factories that instantiate in-memory symfony cache adapters.
|
||||
* This allows the cache adapters to be used in conjunction with filesystem cache.
|
||||
*/
|
||||
interface InMemoryCacheFactory extends CacheFactory
|
||||
{
|
||||
/**
|
||||
* Create a PSR6-compliant cache adapter
|
||||
*/
|
||||
public function createPsr6(string $service, array $params = []): CacheItemPoolInterface;
|
||||
}
|
@ -33,14 +33,9 @@ class ManifestCacheFactory extends DefaultCacheFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: While the returned object is used as a singleton (by the originating Injector->get() call),
|
||||
* this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name.
|
||||
*
|
||||
* @param string $service The class name of the service.
|
||||
* @param array $params The constructor parameters.
|
||||
* @return CacheInterface
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): CacheInterface
|
||||
{
|
||||
// Override default cache generation with SS_MANIFESTCACHE
|
||||
$cacheClass = Environment::getEnv('SS_MANIFESTCACHE');
|
||||
|
@ -2,41 +2,52 @@
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Memcached;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use RuntimeException;
|
||||
use SilverStripe\Core\Environment;
|
||||
|
||||
class MemcachedCacheFactory implements CacheFactory
|
||||
/**
|
||||
* Factory to instantiate a MemcachedAdapter for use in caching.
|
||||
*
|
||||
* SS_MEMCACHED_DSN must be set in environment variables.
|
||||
* See https://symfony.com/doc/current/components/cache/adapters/memcached_adapter.html#configure-the-connection
|
||||
*/
|
||||
class MemcachedCacheFactory extends AbstractCacheFactory implements InMemoryCacheFactory
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Memcached
|
||||
*/
|
||||
protected $memcachedClient;
|
||||
|
||||
/**
|
||||
* @param Memcached $memcachedClient
|
||||
*/
|
||||
public function __construct(Memcached $memcachedClient = null)
|
||||
{
|
||||
$this->memcachedClient = $memcachedClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): CacheInterface
|
||||
{
|
||||
$psr6Cache = $this->createPsr6($service, $params);
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
return $this->prepareCacheForUse($psr6Cache, $useInjector);
|
||||
}
|
||||
|
||||
public function createPsr6(string $service, array $params = []): CacheItemPoolInterface
|
||||
{
|
||||
if (!MemcachedAdapter::isSupported()) {
|
||||
throw new RuntimeException('Memcached is not supported in the current environment. Cannot use Memcached cache.');
|
||||
}
|
||||
|
||||
$dsn = Environment::getEnv('SS_MEMCACHED_DSN');
|
||||
if (!$dsn) {
|
||||
throw new RuntimeException('The SS_MEMCACHED_DSN environment variable must be set to use Memcached cache.');
|
||||
}
|
||||
|
||||
$namespace = isset($params['namespace'])
|
||||
? $params['namespace'] . '_' . md5(BASE_PATH)
|
||||
: md5(BASE_PATH);
|
||||
$defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0;
|
||||
$psr6Cache = Injector::inst()->createWithArgs(MemcachedAdapter::class, [
|
||||
$this->memcachedClient,
|
||||
$namespace,
|
||||
$defaultLifetime
|
||||
]);
|
||||
return Injector::inst()->createWithArgs(Psr16Cache::class, [$psr6Cache]);
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
$client = MemcachedAdapter::createConnection($dsn);
|
||||
|
||||
return $this->instantiateCache(
|
||||
MemcachedAdapter::class,
|
||||
[$client, $namespace, $defaultLifetime],
|
||||
$useInjector
|
||||
);
|
||||
}
|
||||
}
|
||||
|
64
src/Core/Cache/RedisCacheFactory.php
Normal file
64
src/Core/Cache/RedisCacheFactory.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Cache;
|
||||
|
||||
use RuntimeException;
|
||||
use Predis\Client as PredisClient;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Relay\Relay;
|
||||
use SilverStripe\Core\Environment;
|
||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||
|
||||
/**
|
||||
* Factory to instantiate a RedisAdapter for use in caching.
|
||||
*
|
||||
* One of the "redis" PHP extension, the "predis/predis" package, or the "cachewerk/relay" package is required to use Redis.
|
||||
*
|
||||
* SS_REDIS_DSN must be set in environment variables.
|
||||
* See https://symfony.com/doc/current/components/cache/adapters/redis_adapter.html#configure-the-connection
|
||||
*/
|
||||
class RedisCacheFactory extends AbstractCacheFactory implements InMemoryCacheFactory
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create(string $service, array $params = []): CacheInterface
|
||||
{
|
||||
$psr6Cache = $this->createPsr6($service, $params);
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
return $this->prepareCacheForUse($psr6Cache, $useInjector);
|
||||
}
|
||||
|
||||
public function createPsr6(string $service, array $params = []): CacheItemPoolInterface
|
||||
{
|
||||
if (!$this->isSupported()) {
|
||||
throw new RuntimeException('Redis is not supported in the current environment. Cannot use Redis cache.');
|
||||
}
|
||||
|
||||
$dsn = Environment::getEnv('SS_REDIS_DSN');
|
||||
if (!$dsn) {
|
||||
throw new RuntimeException('The SS_REDIS_DSN environment variable must be set to use Redis cache.');
|
||||
}
|
||||
|
||||
$namespace = isset($params['namespace'])
|
||||
? $params['namespace'] . '_' . md5(BASE_PATH)
|
||||
: md5(BASE_PATH);
|
||||
$defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0;
|
||||
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
|
||||
$client = RedisAdapter::createConnection($dsn, ['lazy' => true]);
|
||||
|
||||
return $this->instantiateCache(
|
||||
RedisAdapter::class,
|
||||
[$client, $namespace, $defaultLifetime],
|
||||
$useInjector
|
||||
);
|
||||
}
|
||||
|
||||
public function isSupported()
|
||||
{
|
||||
return class_exists(PredisClient::class) ||
|
||||
class_exists(Relay::class) ||
|
||||
extension_loaded('redis');
|
||||
}
|
||||
}
|
@ -15,5 +15,5 @@ interface Factory
|
||||
* @param array $params The constructor parameters.
|
||||
* @return object The created service instances.
|
||||
*/
|
||||
public function create($service, array $params = []);
|
||||
public function create(string $service, array $params = []): ?object;
|
||||
}
|
||||
|
@ -12,19 +12,10 @@ class InjectionCreator implements Factory
|
||||
/**
|
||||
* Create a new instance of a class
|
||||
*
|
||||
* Passing an object for $class will result from using an anonymous class in unit testing, e.g.
|
||||
* Injector::inst()->load([SomeClass::class => ['class' => new class { ... }]]);
|
||||
*
|
||||
* @param string|object $class - string: The FQCN of the class, object: A class instance
|
||||
*/
|
||||
public function create($class, array $params = [])
|
||||
public function create(string $class, array $params = []): object
|
||||
{
|
||||
if (is_object($class ?? '')) {
|
||||
$class = get_class($class);
|
||||
}
|
||||
if (!is_string($class ?? '')) {
|
||||
throw new InvalidArgumentException('$class parameter must be a string or an object');
|
||||
}
|
||||
if (!class_exists($class)) {
|
||||
throw new InjectorNotFoundException("Class {$class} does not exist");
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Tests\Cache;
|
||||
|
||||
use Behat\Gherkin\Cache\MemoryCache;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Core\Cache\ApcuCacheFactory;
|
||||
use SilverStripe\Core\Cache\MemcachedCacheFactory;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Tests\Cache\CacheTest\MockCache;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
use Symfony\Component\Cache\Adapter\ApcuAdapter;
|
||||
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
|
||||
use Memcached;
|
||||
|
||||
class CacheTest extends SapphireTest
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Injector::inst()
|
||||
->load([
|
||||
ApcuCacheFactory::class => [
|
||||
'constructor' => [ 'version' => 'ss40test' ]
|
||||
],
|
||||
'MemcachedClient' => Memcached::class,
|
||||
MemcachedCacheFactory::class => [
|
||||
'constructor' => [ 'memcachedClient' => '%$MemcachedClient' ]
|
||||
],
|
||||
CacheInterface::class . '.TestApcuCache' => [
|
||||
'factory' => ApcuCacheFactory::class,
|
||||
'constructor' => [
|
||||
'namespace' => 'TestApcuCache',
|
||||
'defaultLifetime' => 2600,
|
||||
],
|
||||
],
|
||||
CacheInterface::class . '.TestMemcache' => [
|
||||
'factory' => MemcachedCacheFactory::class,
|
||||
'constructor' => [
|
||||
'namespace' => 'TestMemCache',
|
||||
'defaultLifetime' => 5600,
|
||||
],
|
||||
],
|
||||
Psr16Cache::class => MockCache::class,
|
||||
ApcuAdapter::class => MockCache::class,
|
||||
MemcachedAdapter::class => MockCache::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testApcuCacheFactory()
|
||||
{
|
||||
$psr16Cache = Injector::inst()->get(CacheInterface::class . '.TestApcuCache');
|
||||
$this->assertInstanceOf(MockCache::class, $psr16Cache);
|
||||
$this->assertEquals(MockCache::class, get_class($psr16Cache->getArgs()[0]));
|
||||
$this->assertEquals(
|
||||
[
|
||||
'TestApcuCache_' . md5(BASE_PATH),
|
||||
2600,
|
||||
'ss40test'
|
||||
],
|
||||
$psr16Cache->getArgs()[0]->getArgs()
|
||||
);
|
||||
}
|
||||
|
||||
public function testMemCacheFactory()
|
||||
{
|
||||
if (!class_exists(Memcached::class)) {
|
||||
$this->markTestSkipped('Memcached is not installed');
|
||||
}
|
||||
$psr16Cache = Injector::inst()->get(CacheInterface::class . '.TestMemcache');
|
||||
$this->assertInstanceOf(MockCache::class, $psr16Cache);
|
||||
$this->assertEquals(MockCache::class, get_class($psr16Cache->getArgs()[0]));
|
||||
$this->assertEquals(
|
||||
[
|
||||
new MemCached(),
|
||||
'TestMemCache_' . md5(BASE_PATH),
|
||||
5600
|
||||
],
|
||||
$psr16Cache->getArgs()[0]->getArgs()
|
||||
);
|
||||
}
|
||||
}
|
156
tests/php/Core/Cache/DefaultCacheFactoryTest.php
Normal file
156
tests/php/Core/Cache/DefaultCacheFactoryTest.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Tests\Cache;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Predis\Client as PredisClient;
|
||||
use ReflectionProperty;
|
||||
use Relay\Relay;
|
||||
use RuntimeException;
|
||||
use SilverStripe\Core\Cache\ApcuCacheFactory;
|
||||
use SilverStripe\Core\Cache\DefaultCacheFactory;
|
||||
use SilverStripe\Core\Cache\MemcachedCacheFactory;
|
||||
use SilverStripe\Core\Cache\RedisCacheFactory;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use Symfony\Component\Cache\Adapter\ApcuAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ChainAdapter;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
|
||||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||
|
||||
class DefaultCacheFactoryTest extends SapphireTest
|
||||
{
|
||||
public function provideCreate(): array
|
||||
{
|
||||
$scenarios = [
|
||||
[
|
||||
'args' => [
|
||||
'useInMemoryCache' => true,
|
||||
'useInjector' => true,
|
||||
],
|
||||
'inMemoryCacheFactory' => null,
|
||||
],
|
||||
[
|
||||
'args' => [
|
||||
'useInMemoryCache' => false,
|
||||
],
|
||||
'inMemoryCacheFactory' => null,
|
||||
],
|
||||
[
|
||||
'args' => [
|
||||
'useInjector' => false,
|
||||
],
|
||||
'inMemoryCacheFactory' => null,
|
||||
],
|
||||
];
|
||||
$cacheFactories = [
|
||||
null,
|
||||
];
|
||||
// Add the in-memory cache factories the current test environment supports
|
||||
// If a factory isn't supported and we add it anyway it'll throw an exception.
|
||||
if (filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL)) {
|
||||
$cacheFactories[] = ApcuCacheFactory::class;
|
||||
}
|
||||
if (MemcachedAdapter::isSupported()) {
|
||||
$cacheFactories[] = MemcachedCacheFactory::class;
|
||||
}
|
||||
if (class_exists(PredisClient::class) ||
|
||||
class_exists(Relay::class) ||
|
||||
extension_loaded('redis')
|
||||
) {
|
||||
$cacheFactories[] = RedisCacheFactory::class;
|
||||
}
|
||||
// Use all of the above test scenarios with each supported cache factory
|
||||
$allScenarios = [];
|
||||
foreach ($cacheFactories as $cacheFactory) {
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['inMemoryCacheFactory'] = $cacheFactory;
|
||||
$allScenarios[] = $scenario;
|
||||
}
|
||||
}
|
||||
return $allScenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCreate
|
||||
*/
|
||||
public function testCreate(array $args, ?string $inMemoryCacheFactory): void
|
||||
{
|
||||
$oldFactoryValue = Environment::getEnv('SS_IN_MEMORY_CACHE_FACTORY');
|
||||
$oldMemcachedDSNValue = Environment::getEnv('SS_MEMCACHED_DSN');
|
||||
$oldRedisDSNValue = Environment::getEnv('SS_REDIS_DSN');
|
||||
Environment::setEnv('SS_IN_MEMORY_CACHE_FACTORY', $inMemoryCacheFactory);
|
||||
// These are obviously not real connections, but it seems a real connection is not required
|
||||
// to just instantiate the cache adapter, which allows us to validate the correct adapter
|
||||
// is instantiated.
|
||||
Environment::setEnv('SS_MEMCACHED_DSN', "memcached://example.com:1234");
|
||||
Environment::setEnv('SS_REDIS_DSN', "redis://password@example.com:1234");
|
||||
|
||||
try {
|
||||
$logger = new Logger('test-cache');
|
||||
$defaultArgs = [
|
||||
'namespace' => __FUNCTION__,
|
||||
'directory' => TEMP_PATH,
|
||||
];
|
||||
$factory = new DefaultCacheFactory($defaultArgs, $logger);
|
||||
$psr16Wrapper = $factory->create('test-cache', $args);
|
||||
|
||||
$reflectionPoolProperty = new ReflectionProperty($psr16Wrapper, 'pool');
|
||||
$reflectionPoolProperty->setAccessible(true);
|
||||
$cacheBucket = $reflectionPoolProperty->getValue($psr16Wrapper);
|
||||
|
||||
if (!$inMemoryCacheFactory || (isset($args['useInMemoryCache']) && !$args['useInMemoryCache'])) {
|
||||
$filesystemCache = $cacheBucket;
|
||||
} else {
|
||||
$this->assertInstanceOf(ChainAdapter::class, $cacheBucket);
|
||||
|
||||
$reflectionAdaptersProperty = new ReflectionProperty($cacheBucket, 'adapters');
|
||||
$reflectionAdaptersProperty->setAccessible(true);
|
||||
$adapters = $reflectionAdaptersProperty->getValue($cacheBucket);
|
||||
|
||||
$this->assertCount(2, $adapters);
|
||||
|
||||
// in-memory cache always comes first
|
||||
$inMemoryCache = array_shift($adapters);
|
||||
$filesystemCache = array_shift($adapters);
|
||||
|
||||
// Check we have the right adapter for the given factory
|
||||
switch ($inMemoryCacheFactory) {
|
||||
case RedisCacheFactory::class:
|
||||
$this->assertInstanceOf(RedisAdapter::class, $inMemoryCache);
|
||||
break;
|
||||
case MemcachedCacheFactory::class:
|
||||
$this->assertInstanceOf(MemcachedAdapter::class, $inMemoryCache);
|
||||
break;
|
||||
case ApcuCacheFactory::class:
|
||||
$this->assertInstanceOf(ApcuAdapter::class, $inMemoryCache);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unexpected factory while running test: $inMemoryCacheFactory");
|
||||
}
|
||||
|
||||
// Check the adapter got the right logger
|
||||
$reflectionLoggerProperty = new ReflectionProperty($inMemoryCache, 'logger');
|
||||
$reflectionLoggerProperty->setAccessible(true);
|
||||
$this->assertTrue($logger === $reflectionLoggerProperty->getValue($inMemoryCache));
|
||||
}
|
||||
|
||||
// Check filesystem cache is correct
|
||||
if (filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOL) && filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOL)) {
|
||||
$this->assertInstanceOf(PhpFilesAdapter::class, $filesystemCache);
|
||||
} else {
|
||||
$this->assertInstanceOf(FilesystemAdapter::class, $filesystemCache);
|
||||
}
|
||||
// Check the adapter got the right logger
|
||||
$reflectionLoggerProperty = new ReflectionProperty($filesystemCache, 'logger');
|
||||
$reflectionLoggerProperty->setAccessible(true);
|
||||
$this->assertTrue($logger === $reflectionLoggerProperty->getValue($filesystemCache));
|
||||
} finally {
|
||||
Environment::setEnv('SS_IN_MEMORY_CACHE_FACTORY', $oldFactoryValue);
|
||||
Environment::setEnv('SS_MEMCACHED_DSN', $oldMemcachedDSNValue);
|
||||
Environment::setEnv('SS_REDIS_DSN', $oldRedisDSNValue);
|
||||
}
|
||||
}
|
||||
}
|
@ -1155,24 +1155,25 @@ class InjectorTest extends SapphireTest
|
||||
|
||||
public function testAnonymousClass()
|
||||
{
|
||||
$class = get_class(new class ('abc') {
|
||||
private string $property;
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->property = $value;
|
||||
}
|
||||
public function foo(): string
|
||||
{
|
||||
return $this->property;
|
||||
}
|
||||
});
|
||||
Injector::inst()->load([
|
||||
'Some\\Project\\Class' => [
|
||||
// the php anonymous class syntax will instantiate a new anonymous class object, with ('abc')
|
||||
// passed to the constructor
|
||||
'class' => new class ('abc') {
|
||||
private string $property;
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->property = $value;
|
||||
}
|
||||
public function foo(): string
|
||||
{
|
||||
return $this->property;
|
||||
}
|
||||
}
|
||||
'class' => $class,
|
||||
],
|
||||
]);
|
||||
// assert that Injector creates a new instance of the anonymous class, with ('def') passed to the constructor
|
||||
$this->assertSame('def', Injector::inst()->create('Some\\Project\\Class', 'def')->foo());
|
||||
$injectedObject = Injector::inst()->create('Some\\Project\\Class', 'def');
|
||||
$this->assertInstanceOf($class, $injectedObject);
|
||||
$this->assertSame('def', $injectedObject->foo());
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use SilverStripe\Core\Injector\Factory;
|
||||
|
||||
class EmptyFactory implements Factory
|
||||
{
|
||||
public function create($service, array $params = [])
|
||||
public function create(string $service, array $params = []): ?object
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class SSObjectCreator extends InjectionCreator implements TestOnly
|
||||
$this->injector = $injector;
|
||||
}
|
||||
|
||||
public function create($class, array $params = [])
|
||||
public function create(string $class, array $params = []): object
|
||||
{
|
||||
if (strpos($class ?? '', '(') === false) {
|
||||
return parent::create($class, $params);
|
||||
|
Loading…
Reference in New Issue
Block a user