[Symfony 4] How to override cache redis adapter for hide warning message
Overview
Sometime you want to custom system cache of symfony (here i want to talk symfony 4.4) for some particular reason. For me, for hide warning message when app connect in redis cluster, some key data in random node, it have moved away node make error warning.
Failed to save key
Create folder Customize/Cache like this
Copy AbstractAdapter from vendor into this , and change namespace for suitable
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Cache\Adapter;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
/**
* @internal
*/
protected const NS_SEPARATOR = ':';
private static $apcuSupported;
private static $phpFilesSupported;
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $v = $value;
$item->isHit = $isHit;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
return $item;
},
null,
CacheItem::class
);
$getId = \Closure::fromCallable([$this, 'getId']);
$this->mergeByLifetime = \Closure::bind(
static function ($deferred, $namespace, &$expiredIds) use ($getId, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
} elseif (!$item->expiry) {
$ttl = 0;
} elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value;
}
return $byLifetime;
},
null,
CacheItem::class
);
}
/**
* Returns the best possible adapter that your runtime supports.
*
* Using ApcuAdapter makes system caches compatible with read-only filesystems.
*
* @param string $namespace
* @param int $defaultLifetime
* @param string $version
* @param string $directory
*
* @return AdapterInterface
*/
public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null)
{
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
if (null !== $logger) {
$opcache->setLogger($logger);
}
if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) {
return $opcache;
}
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
return $opcache;
}
$apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
if (null !== $logger) {
$apcu->setLogger($logger);
}
return new ChainAdapter([$apcu, $opcache]);
}
public static function createConnection($dsn, array $options = [])
{
if (!\is_string($dsn)) {
throw new InvalidArgumentException(sprintf('The "%s()" method expect argument #1 to be string, "%s" given.', __METHOD__, \gettype($dsn)));
}
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
$ok = true;
$byLifetime = $this->mergeByLifetime;
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
$retry = $this->deferred = [];
if ($expiredIds) {
try {
$this->doDelete($expiredIds);
} catch (\Exception $e) {
$ok = false;
CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
}
foreach ($byLifetime as $lifetime => $values) {
try {
$e = $this->doSave($values, $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
// $type = \is_object($v) ? \get_class($v) : \gettype($v);
// $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
// CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}
// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$e = $this->doSave([$id => $v], $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
// $type = \is_object($v) ? \get_class($v) : \gettype($v);
// $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
// CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
}
return $ok;
}
}
Create class Cache for shortend (like Facade pattern in Laravel)
<?php
namespace Customize\Cache;
use Customize\Common\Constant;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Throwable;
use function log_error;
class Cache
{
/**
* @var AdapterInterface
*/
private AdapterInterface $cache;
public function __construct(ContainerInterface $containerInterface)
{
$this->cache = $containerInterface->get('cache.adapter.redis.custom');
}
/**
* @param string $cacheKey
* @param object $classObj
* @param string $func
*
* @param array $args
*
* @return mixed
*/
public function get(string $cacheKey, object $classObj, string $func, array $args = [])
{
try {
$value = $this->cache->get($cacheKey, function (ItemInterface $item) use ($classObj, $func, $args) {
$item->expiresAfter(Constant::CACHE_EXPIRE_TIME);
if (method_exists($classObj, $func)) {
return call_user_func_array([$classObj, $func], $args);
}
$className = get_class($classObj);
throw new Exception("Call method not found $className::$func");
});
} catch (Throwable $exception) {
log_error("Get cache with key ($cacheKey) fails. ", [$exception]);
return null;
}
return $value;
}
/**
* @param string $cacheKey
* @return bool
* @throws \Psr\Cache\InvalidArgumentException
*/
public function delete(string $cacheKey): bool
{
try {
if ($this->cache->hasItem($cacheKey)) {
$this->cache->delete($cacheKey);
}
} catch (Throwable $exception) {
log_error("Delete cache with key ($cacheKey) fails. ", [$exception]);
return false;
}
return true;
}
}
Create RedisAdapter and init redis instance
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Cache;
use Customize\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redis = null, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
if (env('IS_REDIS_CLUSTER') == 1) {
$redis = new \RedisCluster(null, [env('REDIS_HOST') . ':' . env('REDIS_PORT')]);
} else {
$redis = new \Redis();
$redis->pconnect(env('REDIS_HOST'), env('REDIS_PORT'));
}
$this->init($redis, $namespace, $defaultLifetime, $marshaller);
}
}
Create .env for redis
## Redis
IS_REDIS_CLUSTER=0
REDIS_HOST=localhost
REDIS_PORT=6379
Init RedisAdapter in services.yaml
cache.adapter.redis.custom:
class: Customize\Cache\RedisAdapter
decorates: cache.adapter.redis
public: true
So it’s almost done !!!
How to use it
Instance Customize\Cache\Cache in __construct, and use it for cache data.
/**
* @param Cache $cache
*/
public function __construct(
Cache $cache
) {
$this->cache = $cache;
}
$dataMenu = $this->cache->get(
Constant::SALE_LEFT_SIDEBAR_CACHE_KEY,
$menuService,
'getSidebarLeft'
);
Thank you for reading. Hope it is helpful for you :) !!!