From 32dafbb1cbe0c260ea5573a0c4cab74ad4603c9b Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Tue, 4 Nov 2025 13:57:22 +0000 Subject: [PATCH] cache fallbacks for unsupported drivers Signed-off-by: Andy Miller --- system/src/Grav/Common/Cache.php | 138 ++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 32 deletions(-) diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php index 39c0a8b41..45d0627c3 100644 --- a/system/src/Grav/Common/Cache.php +++ b/system/src/Grav/Common/Cache.php @@ -20,12 +20,14 @@ use LogicException; use Psr\SimpleCache\CacheInterface; use RocketTheme\Toolbox\Event\Event; use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\MemcachedAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\EventDispatcher\EventDispatcher; +use Throwable; use function dirname; use function extension_loaded; use function function_exists; @@ -295,10 +297,28 @@ class Cache extends Getters public function getCacheAdapter(?string $namespace = null, ?int $defaultLifetime = null): AdapterInterface { $setting = $this->driver_setting ?? 'auto'; + $original_setting = $setting; $driver_name = 'file'; + $adapter = null; + $compatibility = [ + 'filesystem' => 'file', + 'files' => 'file', + 'doctrine' => 'file', + 'apc' => 'apcu', + 'memcache' => 'memcached', + ]; - if (in_array($setting, ['apc', 'xcache', 'wincache', 'memcache'], true)) { - throw new LogicException(sprintf('Cache driver for %s has been removed, use auto, file, apcu or memcached instead!', $setting)); + if (isset($compatibility[$setting])) { + $mapped = $compatibility[$setting]; + if ($mapped !== $setting) { + $this->logCacheFallback($original_setting, $mapped, 'legacy cache driver detected'); + } + $setting = $mapped; + } + + if (in_array($setting, ['xcache', 'wincache'], true)) { + $this->logCacheFallback($original_setting, 'file', 'unsupported cache driver removed in Grav 1.8'); + $setting = 'file'; } // CLI compatibility requires a non-volatile cache driver @@ -314,69 +334,123 @@ class Cache extends Getters $driver_name = $setting; } - $this->driver_name = $driver_name; $namespace ??= $this->key; $defaultLifetime ??= 0; + $resolved_driver_name = $driver_name; switch ($driver_name) { case 'apc': case 'apcu': - $adapter = new ApcuAdapter($namespace, $defaultLifetime); + if (extension_loaded('apcu')) { + $adapter = new ApcuAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'apcu'; + } else { + $this->logCacheFallback($driver_name, 'file', 'APCu extension not loaded'); + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; + } break; case 'memcached': if (extension_loaded('memcached')) { $memcached = new \Memcached(); - $memcached->addServer( + $connected = $memcached->addServer( $this->config->get('system.cache.memcached.server', 'localhost'), $this->config->get('system.cache.memcached.port', 11211) ); - $adapter = new MemcachedAdapter($memcached, $namespace, $defaultLifetime); + if ($connected) { + $adapter = new MemcachedAdapter($memcached, $namespace, $defaultLifetime); + $resolved_driver_name = 'memcached'; + } else { + $this->logCacheFallback($driver_name, 'file', 'Memcached server configuration failed'); + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; + } } else { - throw new LogicException('Memcached PHP extension has not been installed'); + $this->logCacheFallback($driver_name, 'file', 'Memcached extension not installed'); + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; } break; case 'redis': if (extension_loaded('redis')) { $redis = new \Redis(); - $socket = $this->config->get('system.cache.redis.socket', false); - $password = $this->config->get('system.cache.redis.password', false); - $databaseId = $this->config->get('system.cache.redis.database', 0); + try { + $socket = $this->config->get('system.cache.redis.socket', false); + $password = $this->config->get('system.cache.redis.password', false); + $databaseId = $this->config->get('system.cache.redis.database', 0); - if ($socket) { - $redis->connect($socket); - } else { - $redis->connect( - $this->config->get('system.cache.redis.server', 'localhost'), - $this->config->get('system.cache.redis.port', 6379) - ); + if ($socket) { + $redis->connect($socket); + } else { + $redis->connect( + $this->config->get('system.cache.redis.server', 'localhost'), + $this->config->get('system.cache.redis.port', 6379) + ); + } + + // Authenticate with password if set + if ($password && !$redis->auth($password)) { + throw new \RedisException('Redis authentication failed'); + } + + // Select alternate ( !=0 ) database ID if set + if ($databaseId && !$redis->select($databaseId)) { + throw new \RedisException('Could not select alternate Redis database ID'); + } + + $adapter = new RedisAdapter($redis, $namespace, $defaultLifetime); + $resolved_driver_name = 'redis'; + } catch (Throwable $e) { + $this->logCacheFallback($driver_name, 'file', $e->getMessage()); + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; } - - // Authenticate with password if set - if ($password && !$redis->auth($password)) { - throw new \RedisException('Redis authentication failed'); - } - - // Select alternate ( !=0 ) database ID if set - if ($databaseId && !$redis->select($databaseId)) { - throw new \RedisException('Could not select alternate Redis database ID'); - } - - $adapter = new RedisAdapter($redis, $namespace, $defaultLifetime); } else { - throw new LogicException('Redis PHP extension has not been installed'); + $this->logCacheFallback($driver_name, 'file', 'Redis extension not installed'); + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; } break; + case 'array': + $adapter = new ArrayAdapter($defaultLifetime, false); + $adapter->setNamespace($namespace); + $resolved_driver_name = 'array'; + break; + default: - $adapter = new FilesystemAdapter($namespace, $defaultLifetime, $this->cache_dir); + if (!in_array($driver_name, ['file', 'filesystem'], true)) { + $this->logCacheFallback($driver_name, 'file', 'unknown cache driver'); + } + $adapter = $this->createFilesystemAdapter($namespace, $defaultLifetime); + $resolved_driver_name = 'file'; break; } + $this->driver_name = $resolved_driver_name; + return $adapter; } + protected function createFilesystemAdapter(string $namespace, int $defaultLifetime): FilesystemAdapter + { + return new FilesystemAdapter($namespace, $defaultLifetime, $this->cache_dir); + } + + protected function logCacheFallback(string $from, string $to, string $reason): void + { + try { + $log = Grav::instance()['log'] ?? null; + if ($log) { + $log->warning(sprintf('Cache driver "%s" unavailable (%s); falling back to "%s".', $from, $reason, $to)); + } + } catch (Throwable) { + // Logging failed, continue silently. + } + } + /** * Automatically picks the cache mechanism to use. If you pick one manually it will use that * If there is no config option for $driver in the config, or it's set to 'auto', it will @@ -698,7 +772,7 @@ class Cache extends Getters */ public function isVolatileDriver($setting) { - return $setting === 'apcu'; + return in_array($setting, ['apcu', 'array'], true); } /**