Skip to content

Commit

Permalink
FIX Ensure cache is shared between CLI and webserver
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Jul 4, 2024
1 parent d96d852 commit 839cb92
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 183 deletions.
1 change: 0 additions & 1 deletion _config/cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Control/Email/TransportFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
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'];
Expand Down
100 changes: 100 additions & 0 deletions src/Core/Cache/AbstractCacheFactory.php
Original file line number Diff line number Diff line change
@@ -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, $loggerAdded);
// 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, $loggerAdded);
}
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);
}

private function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool
{
if ($this->logger && ($cache instanceof LoggerAwareInterface)) {
$cache->setLogger($this->logger);
return true;
}
return false;
}
}
65 changes: 44 additions & 21 deletions src/Core/Cache/ApcuCacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
* @inheritdoc
*/
public function __construct($version = null)
public function create(string $service, array $params = []): CacheInterface
{
$this->version = $version;
$psr6Cache = $this->createPsr6($service, $params);
$useInjector = isset($params['useInjector']) ? $params['useInjector'] : true;
return $this->prepareCacheForUse($psr6Cache, $useInjector);
}

/**
* @inheritdoc
*/
public function create($service, array $params = [])
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 changes: 1 addition & 6 deletions src/Core/Cache/CacheFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@

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;
}
Loading

0 comments on commit 839cb92

Please sign in to comment.