diff --git a/Adapter/AbstractAdapter.php b/Adapter/AbstractAdapter.php
index c03868da..2b4bc8b2 100644
--- a/Adapter/AbstractAdapter.php
+++ b/Adapter/AbstractAdapter.php
@@ -19,11 +19,12 @@
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
/**
* @author Nicolas Grekas
*/
-abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
+abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
@@ -37,7 +38,19 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
- $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
+ if ('' !== $namespace) {
+ if (str_contains($namespace, static::NS_SEPARATOR)) {
+ if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
+ throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
+ }
+ CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
+ } else {
+ CacheItem::validateKey($namespace);
+ }
+ $this->namespace = $namespace.static::NS_SEPARATOR;
+ }
+ $this->rootNamespace = $this->namespace;
+
$this->defaultLifetime = $defaultLifetime;
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));
@@ -111,14 +124,14 @@ public static function createSystemCache(string $namespace, int $defaultLifetime
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
{
- if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
+ if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'couchbase:')) {
- if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) {
+ if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) {
return CouchbaseBucketAdapter::createConnection($dsn, $options);
}
@@ -128,7 +141,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
return PdoAdapter::createConnection($dsn, $options);
}
- throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
+ throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
}
public function commit(): bool
@@ -159,7 +172,7 @@ public function commit(): bool
$v = $values[$id];
$type = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
+ CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
} else {
foreach ($values as $id => $v) {
@@ -182,7 +195,7 @@ public function commit(): bool
$ok = false;
$type = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
+ CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
}
diff --git a/Adapter/AbstractTagAwareAdapter.php b/Adapter/AbstractTagAwareAdapter.php
index 822c30f0..23db2b6e 100644
--- a/Adapter/AbstractTagAwareAdapter.php
+++ b/Adapter/AbstractTagAwareAdapter.php
@@ -17,6 +17,7 @@
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
@@ -30,16 +31,33 @@
*
* @internal
*/
-abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
+abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, AdapterInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
+ /**
+ * @internal
+ */
+ protected const NS_SEPARATOR = ':';
+
private const TAGS_PREFIX = "\1tags\1";
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
- $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
+ if ('' !== $namespace) {
+ if (str_contains($namespace, static::NS_SEPARATOR)) {
+ if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
+ throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
+ }
+ CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
+ } else {
+ CacheItem::validateKey($namespace);
+ }
+ $this->namespace = $namespace.static::NS_SEPARATOR;
+ }
+ $this->rootNamespace = $this->namespace;
+
$this->defaultLifetime = $defaultLifetime;
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));
@@ -70,7 +88,7 @@ static function ($key, $value, $isHit) {
CacheItem::class
);
self::$mergeByLifetime ??= \Closure::bind(
- static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
+ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime, $rootNamespace) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
@@ -102,10 +120,10 @@ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime)
$value['tag-operations'] = ['add' => [], 'remove' => []];
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
- $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
+ $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag, $rootNamespace);
}
foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
- $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
+ $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag, $rootNamespace);
}
$value['tags'] = array_keys($value['tags']);
@@ -168,7 +186,7 @@ protected function doDeleteYieldTags(array $ids): iterable
public function commit(): bool
{
$ok = true;
- $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
+ $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime, $this->rootNamespace);
$retry = $this->deferred = [];
if ($expiredIds) {
@@ -195,7 +213,7 @@ public function commit(): bool
$v = $values[$id];
$type = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
+ CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
} else {
foreach ($values as $id => $v) {
@@ -219,7 +237,7 @@ public function commit(): bool
$ok = false;
$type = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
+ CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
}
@@ -244,7 +262,7 @@ public function deleteItems(array $keys): bool
try {
foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
foreach ($tags as $tag) {
- $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
+ $tagData[$this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace)][] = $id;
}
}
} catch (\Exception) {
@@ -283,7 +301,7 @@ public function invalidateTags(array $tags): bool
$tagIds = [];
foreach (array_unique($tags) as $tag) {
- $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
+ $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace);
}
try {
diff --git a/Adapter/ArrayAdapter.php b/Adapter/ArrayAdapter.php
index 8941ae1c..c9ee94ee 100644
--- a/Adapter/ArrayAdapter.php
+++ b/Adapter/ArrayAdapter.php
@@ -19,6 +19,7 @@
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
/**
* An in-memory cache storage.
@@ -27,13 +28,14 @@
*
* @author Nicolas Grekas
*/
-class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
+class ArrayAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
{
use LoggerAwareTrait;
private array $values = [];
private array $tags = [];
private array $expiries = [];
+ private array $subPools = [];
private static \Closure $createCacheItem;
@@ -226,16 +228,38 @@ public function clear(string $prefix = ''): bool
}
}
- if ($this->values) {
- return true;
- }
+ return true;
+ }
+
+ foreach ($this->subPools as $pool) {
+ $pool->clear();
}
- $this->values = $this->tags = $this->expiries = [];
+ $this->subPools = $this->values = $this->tags = $this->expiries = [];
return true;
}
+ public function withSubNamespace(string $namespace): static
+ {
+ CacheItem::validateKey($namespace);
+
+ $subPools = $this->subPools;
+
+ if (isset($subPools[$namespace])) {
+ return $subPools[$namespace];
+ }
+
+ $this->subPools = [];
+ $clone = clone $this;
+ $clone->clear();
+
+ $subPools[$namespace] = $clone;
+ $this->subPools = $subPools;
+
+ return $clone;
+ }
+
/**
* Returns all cached values, with cache miss as null.
*/
@@ -263,6 +287,13 @@ public function reset(): void
$this->clear();
}
+ public function __clone()
+ {
+ foreach ($this->subPools as $i => $pool) {
+ $this->subPools[$i] = clone $pool;
+ }
+ }
+
private function generateItems(array $keys, float $now, \Closure $f): \Generator
{
foreach ($keys as $i => $key) {
diff --git a/Adapter/ChainAdapter.php b/Adapter/ChainAdapter.php
index 09fcfdcc..c27faeb1 100644
--- a/Adapter/ChainAdapter.php
+++ b/Adapter/ChainAdapter.php
@@ -14,11 +14,13 @@
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
@@ -29,7 +31,7 @@
*
* @author Kévin Dunglas
*/
-class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
@@ -280,6 +282,23 @@ public function prune(): bool
return $pruned;
}
+ public function withSubNamespace(string $namespace): static
+ {
+ $clone = clone $this;
+ $adapters = [];
+
+ foreach ($this->adapters as $adapter) {
+ if (!$adapter instanceof NamespacedPoolInterface) {
+ throw new BadMethodCallException('All adapters must implement NamespacedPoolInterface to support namespaces.');
+ }
+
+ $adapters[] = $adapter->withSubNamespace($namespace);
+ }
+ $clone->adapters = $adapters;
+
+ return $clone;
+ }
+
public function reset(): void
{
foreach ($this->adapters as $adapter) {
diff --git a/Adapter/DoctrineDbalAdapter.php b/Adapter/DoctrineDbalAdapter.php
index d67464a4..8e52dfee 100644
--- a/Adapter/DoctrineDbalAdapter.php
+++ b/Adapter/DoctrineDbalAdapter.php
@@ -338,17 +338,17 @@ protected function doSave(array $values, int $lifetime): array|bool
/**
* @internal
*/
- protected function getId(mixed $key): string
+ protected function getId(mixed $key, ?string $namespace = null): string
{
if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
- return parent::getId($key);
+ return parent::getId($key, $namespace);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
- return parent::getId($key);
+ return parent::getId($key, $namespace);
}
private function getPlatformName(): string
@@ -359,9 +359,16 @@ private function getPlatformName(): string
$platform = $this->conn->getDatabasePlatform();
+ if (interface_exists(DBALException::class)) {
+ // DBAL 4+
+ $sqlitePlatformClass = 'Doctrine\DBAL\Platforms\SQLitePlatform';
+ } else {
+ $sqlitePlatformClass = 'Doctrine\DBAL\Platforms\SqlitePlatform';
+ }
+
return $this->platformName = match (true) {
$platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform => 'mysql',
- $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'sqlite',
+ $platform instanceof $sqlitePlatformClass => 'sqlite',
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'pgsql',
$platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => 'oci',
$platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => 'sqlsrv',
diff --git a/Adapter/NullAdapter.php b/Adapter/NullAdapter.php
index d5d2ef6b..35553ea1 100644
--- a/Adapter/NullAdapter.php
+++ b/Adapter/NullAdapter.php
@@ -14,11 +14,12 @@
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
/**
* @author Titouan Galopin
*/
-class NullAdapter implements AdapterInterface, CacheInterface
+class NullAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface
{
private static \Closure $createCacheItem;
@@ -94,6 +95,11 @@ public function delete(string $key): bool
return $this->deleteItem($key);
}
+ public function withSubNamespace(string $namespace): static
+ {
+ return clone $this;
+ }
+
private function generateItems(array $keys): \Generator
{
$f = self::$createCacheItem;
diff --git a/Adapter/PdoAdapter.php b/Adapter/PdoAdapter.php
index 525e2c6d..7d6cb2df 100644
--- a/Adapter/PdoAdapter.php
+++ b/Adapter/PdoAdapter.php
@@ -348,17 +348,17 @@ protected function doSave(array $values, int $lifetime): array|bool
/**
* @internal
*/
- protected function getId(mixed $key): string
+ protected function getId(mixed $key, ?string $namespace = null): string
{
if ('pgsql' !== $this->getDriver()) {
- return parent::getId($key);
+ return parent::getId($key, $namespace);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
- return parent::getId($key);
+ return parent::getId($key, $namespace);
}
private function getConnection(): \PDO
diff --git a/Adapter/PhpFilesAdapter.php b/Adapter/PhpFilesAdapter.php
index df0d0e71..e64ec4c3 100644
--- a/Adapter/PhpFilesAdapter.php
+++ b/Adapter/PhpFilesAdapter.php
@@ -103,65 +103,65 @@ protected function doFetch(array $ids): iterable
}
$values = [];
- begin:
- $getExpiry = false;
-
- foreach ($ids as $id) {
- if (null === $value = $this->values[$id] ?? null) {
- $missingIds[] = $id;
- } elseif ('N;' === $value) {
- $values[$id] = null;
- } elseif (!\is_object($value)) {
- $values[$id] = $value;
- } elseif (!$value instanceof LazyValue) {
- $values[$id] = $value();
- } elseif (false === $values[$id] = include $value->file) {
- unset($values[$id], $this->values[$id]);
- $missingIds[] = $id;
+ while (true) {
+ $getExpiry = false;
+
+ foreach ($ids as $id) {
+ if (null === $value = $this->values[$id] ?? null) {
+ $missingIds[] = $id;
+ } elseif ('N;' === $value) {
+ $values[$id] = null;
+ } elseif (!\is_object($value)) {
+ $values[$id] = $value;
+ } elseif (!$value instanceof LazyValue) {
+ $values[$id] = $value();
+ } elseif (false === $values[$id] = include $value->file) {
+ unset($values[$id], $this->values[$id]);
+ $missingIds[] = $id;
+ }
+ if (!$this->appendOnly) {
+ unset($this->values[$id]);
+ }
}
- if (!$this->appendOnly) {
- unset($this->values[$id]);
+
+ if (!$missingIds) {
+ return $values;
}
- }
- if (!$missingIds) {
- return $values;
- }
+ set_error_handler($this->includeHandler);
+ try {
+ $getExpiry = true;
- set_error_handler($this->includeHandler);
- try {
- $getExpiry = true;
+ foreach ($missingIds as $k => $id) {
+ try {
+ $file = $this->files[$id] ??= $this->getFile($id);
- foreach ($missingIds as $k => $id) {
- try {
- $file = $this->files[$id] ??= $this->getFile($id);
+ if (isset(self::$valuesCache[$file])) {
+ [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
+ } elseif (\is_array($expiresAt = include $file)) {
+ if ($this->appendOnly) {
+ self::$valuesCache[$file] = $expiresAt;
+ }
- if (isset(self::$valuesCache[$file])) {
- [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
- } elseif (\is_array($expiresAt = include $file)) {
- if ($this->appendOnly) {
- self::$valuesCache[$file] = $expiresAt;
+ [$expiresAt, $this->values[$id]] = $expiresAt;
+ } elseif ($now < $expiresAt) {
+ $this->values[$id] = new LazyValue($file);
}
- [$expiresAt, $this->values[$id]] = $expiresAt;
- } elseif ($now < $expiresAt) {
- $this->values[$id] = new LazyValue($file);
- }
-
- if ($now >= $expiresAt) {
- unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
+ if ($now >= $expiresAt) {
+ unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
+ }
+ } catch (\ErrorException $e) {
+ unset($missingIds[$k]);
}
- } catch (\ErrorException $e) {
- unset($missingIds[$k]);
}
+ } finally {
+ restore_error_handler();
}
- } finally {
- restore_error_handler();
- }
- $ids = $missingIds;
- $missingIds = [];
- goto begin;
+ $ids = $missingIds;
+ $missingIds = [];
+ }
}
protected function doHave(string $id): bool
diff --git a/Adapter/ProxyAdapter.php b/Adapter/ProxyAdapter.php
index 56212260..d692dbf3 100644
--- a/Adapter/ProxyAdapter.php
+++ b/Adapter/ProxyAdapter.php
@@ -19,11 +19,12 @@
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
/**
* @author Nicolas Grekas
*/
-class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class ProxyAdapter implements AdapterInterface, NamespacedPoolInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
use ProxyTrait;
@@ -38,12 +39,17 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
- $this->pool = $pool;
- $this->poolHash = spl_object_hash($pool);
if ('' !== $namespace) {
- \assert('' !== CacheItem::validateKey($namespace));
- $this->namespace = $namespace;
+ if ($pool instanceof NamespacedPoolInterface) {
+ $pool = $pool->withSubNamespace($namespace);
+ $this->namespace = $namespace = '';
+ } else {
+ \assert('' !== CacheItem::validateKey($namespace));
+ $this->namespace = $namespace;
+ }
}
+ $this->pool = $pool;
+ $this->poolHash = spl_object_hash($pool);
$this->namespaceLen = \strlen($namespace);
$this->defaultLifetime = $defaultLifetime;
self::$createCacheItem ??= \Closure::bind(
@@ -158,6 +164,20 @@ public function commit(): bool
return $this->pool->commit();
}
+ public function withSubNamespace(string $namespace): static
+ {
+ $clone = clone $this;
+
+ if ($clone->pool instanceof NamespacedPoolInterface) {
+ $clone->pool = $clone->pool->withSubNamespace($namespace);
+ } else {
+ $clone->namespace .= CacheItem::validateKey($namespace);
+ $clone->namespaceLen = \strlen($clone->namespace);
+ }
+
+ return $clone;
+ }
+
private function doSave(CacheItemInterface $item, string $method): bool
{
if (!$item instanceof CacheItem) {
diff --git a/Adapter/RedisAdapter.php b/Adapter/RedisAdapter.php
index e33f2f65..f31f0d7d 100644
--- a/Adapter/RedisAdapter.php
+++ b/Adapter/RedisAdapter.php
@@ -18,7 +18,7 @@ class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
- public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
+ public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay|\Relay\Cluster $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
$this->init($redis, $namespace, $defaultLifetime, $marshaller);
}
diff --git a/Adapter/RedisTagAwareAdapter.php b/Adapter/RedisTagAwareAdapter.php
index 7b282375..779c4d91 100644
--- a/Adapter/RedisTagAwareAdapter.php
+++ b/Adapter/RedisTagAwareAdapter.php
@@ -60,7 +60,7 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
private string $redisEvictionPolicy;
public function __construct(
- \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
+ \Redis|Relay|\Relay\Cluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
private string $namespace = '',
int $defaultLifetime = 0,
?MarshallerInterface $marshaller = null,
@@ -69,7 +69,7 @@ public function __construct(
throw new InvalidArgumentException(\sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
}
- $isRelay = $redis instanceof Relay;
+ $isRelay = $redis instanceof Relay || $redis instanceof \Relay\Cluster;
if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) {
$compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION);
@@ -159,7 +159,7 @@ protected function doDeleteYieldTags(array $ids): iterable
foreach ($results as $id => $result) {
if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
- CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
+ CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $result]);
continue;
}
@@ -225,7 +225,7 @@ protected function doInvalidate(array $tagIds): bool
$results = $this->pipeline(function () use ($tagIds, $lua) {
if ($this->redis instanceof \Predis\ClientInterface) {
$prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
- } elseif (\is_array($prefix = $this->redis->getOption($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
+ } elseif (\is_array($prefix = $this->redis->getOption(($this->redis instanceof Relay || $this->redis instanceof \Relay\Cluster) ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
$prefix = current($prefix);
}
diff --git a/Adapter/TagAwareAdapter.php b/Adapter/TagAwareAdapter.php
index 53c98904..70927cf4 100644
--- a/Adapter/TagAwareAdapter.php
+++ b/Adapter/TagAwareAdapter.php
@@ -16,9 +16,11 @@
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
@@ -33,7 +35,7 @@
* @author Nicolas Grekas
* @author Sergey Belyshkin
*/
-class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
+class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
{
use ContractsTrait;
use LoggerAwareTrait;
@@ -277,6 +279,23 @@ public function commit(): bool
return $ok;
}
+ /**
+ * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface
+ */
+ public function withSubNamespace(string $namespace): static
+ {
+ if (!$this->pool instanceof NamespacedPoolInterface) {
+ throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class));
+ }
+
+ $knownTagVersions = &$this->knownTagVersions; // ensures clones share the same array
+ $clone = clone $this;
+ $clone->deferred = [];
+ $clone->pool = $this->pool->withSubNamespace($namespace);
+
+ return $clone;
+ }
+
public function prune(): bool
{
return $this->pool instanceof PruneableInterface && $this->pool->prune();
diff --git a/Adapter/TraceableAdapter.php b/Adapter/TraceableAdapter.php
index 8fe6cf37..3e1bf2bf 100644
--- a/Adapter/TraceableAdapter.php
+++ b/Adapter/TraceableAdapter.php
@@ -13,9 +13,11 @@
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\BadMethodCallException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
@@ -25,19 +27,27 @@
* @author Tobias Nyholm
* @author Nicolas Grekas
*/
-class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
+class TraceableAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
{
+ private string $namespace = '';
private array $calls = [];
public function __construct(
protected AdapterInterface $pool,
+ protected readonly ?\Closure $disabled = null,
) {
}
+ /**
+ * @throws BadMethodCallException When the item pool is not a CacheInterface
+ */
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
{
if (!$this->pool instanceof CacheInterface) {
- throw new \BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
+ throw new BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
+ }
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->get($key, $callback, $beta, $metadata);
}
$isHit = true;
@@ -65,6 +75,9 @@ public function get(string $key, callable $callback, ?float $beta = null, ?array
public function getItem(mixed $key): CacheItem
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->getItem($key);
+ }
$event = $this->start(__FUNCTION__);
try {
$item = $this->pool->getItem($key);
@@ -82,6 +95,9 @@ public function getItem(mixed $key): CacheItem
public function hasItem(mixed $key): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->hasItem($key);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->hasItem($key);
@@ -92,6 +108,9 @@ public function hasItem(mixed $key): bool
public function deleteItem(mixed $key): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->deleteItem($key);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
@@ -102,6 +121,9 @@ public function deleteItem(mixed $key): bool
public function save(CacheItemInterface $item): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->save($item);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->save($item);
@@ -112,6 +134,9 @@ public function save(CacheItemInterface $item): bool
public function saveDeferred(CacheItemInterface $item): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->saveDeferred($item);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
@@ -122,6 +147,9 @@ public function saveDeferred(CacheItemInterface $item): bool
public function getItems(array $keys = []): iterable
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->getItems($keys);
+ }
$event = $this->start(__FUNCTION__);
try {
$result = $this->pool->getItems($keys);
@@ -145,6 +173,9 @@ public function getItems(array $keys = []): iterable
public function clear(string $prefix = ''): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->clear($prefix);
+ }
$event = $this->start(__FUNCTION__);
try {
if ($this->pool instanceof AdapterInterface) {
@@ -159,6 +190,9 @@ public function clear(string $prefix = ''): bool
public function deleteItems(array $keys): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->deleteItems($keys);
+ }
$event = $this->start(__FUNCTION__);
$event->result['keys'] = $keys;
try {
@@ -170,6 +204,9 @@ public function deleteItems(array $keys): bool
public function commit(): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->commit();
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->commit();
@@ -183,6 +220,9 @@ public function prune(): bool
if (!$this->pool instanceof PruneableInterface) {
return false;
}
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->prune();
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->prune();
@@ -202,6 +242,9 @@ public function reset(): void
public function delete(string $key): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->deleteItem($key);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
@@ -225,11 +268,29 @@ public function getPool(): AdapterInterface
return $this->pool;
}
+ /**
+ * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface
+ */
+ public function withSubNamespace(string $namespace): static
+ {
+ if (!$this->pool instanceof NamespacedPoolInterface) {
+ throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class));
+ }
+
+ $calls = &$this->calls; // ensures clones share the same array
+ $clone = clone $this;
+ $clone->namespace .= CacheItem::validateKey($namespace).':';
+ $clone->pool = $this->pool->withSubNamespace($namespace);
+
+ return $clone;
+ }
+
protected function start(string $name): TraceableAdapterEvent
{
$this->calls[] = $event = new TraceableAdapterEvent();
$event->name = $name;
$event->start = microtime(true);
+ $event->namespace = $this->namespace;
return $event;
}
@@ -246,4 +307,5 @@ class TraceableAdapterEvent
public array|bool $result;
public int $hits = 0;
public int $misses = 0;
+ public string $namespace;
}
diff --git a/Adapter/TraceableTagAwareAdapter.php b/Adapter/TraceableTagAwareAdapter.php
index c85d199e..bde27c68 100644
--- a/Adapter/TraceableTagAwareAdapter.php
+++ b/Adapter/TraceableTagAwareAdapter.php
@@ -18,13 +18,16 @@
*/
class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface
{
- public function __construct(TagAwareAdapterInterface $pool)
+ public function __construct(TagAwareAdapterInterface $pool, ?\Closure $disabled = null)
{
- parent::__construct($pool);
+ parent::__construct($pool, $disabled);
}
public function invalidateTags(array $tags): bool
{
+ if ($this->disabled?->__invoke()) {
+ return $this->pool->invalidateTags($tags);
+ }
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->invalidateTags($tags);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 038915c4..d7b18246 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
CHANGELOG
=========
+7.3
+---
+
+ * Add support for `\Relay\Cluster` in `RedisAdapter`
+ * Add support for `valkey:` / `valkeys:` schemes
+ * Add support for namespace-based invalidation
+ * Rename options "redis_cluster" and "redis_sentinel" to "cluster" and "sentinel" respectively
+
7.2
---
diff --git a/DependencyInjection/CacheCollectorPass.php b/DependencyInjection/CacheCollectorPass.php
index ed957406..0b8d6aed 100644
--- a/DependencyInjection/CacheCollectorPass.php
+++ b/DependencyInjection/CacheCollectorPass.php
@@ -52,7 +52,7 @@ private function addToCollector(string $id, string $name, ContainerBuilder $cont
if (!$definition->isPublic() || !$definition->isPrivate()) {
$recorder->setPublic($definition->isPublic());
}
- $recorder->setArguments([new Reference($innerId = $id.'.recorder_inner')]);
+ $recorder->setArguments([new Reference($innerId = $id.'.recorder_inner'), new Reference('profiler.is_disabled_state_checker', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)]);
foreach ($definition->getMethodCalls() as [$method, $args]) {
if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) {
diff --git a/Exception/BadMethodCallException.php b/Exception/BadMethodCallException.php
new file mode 100644
index 00000000..d81f9d26
--- /dev/null
+++ b/Exception/BadMethodCallException.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Exception;
+
+use Psr\Cache\CacheException as Psr6CacheInterface;
+use Psr\SimpleCache\CacheException as SimpleCacheInterface;
+
+if (interface_exists(SimpleCacheInterface::class)) {
+ class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface, SimpleCacheInterface
+ {
+ }
+} else {
+ class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface
+ {
+ }
+}
diff --git a/Tests/Adapter/AbstractRedisAdapterTestCase.php b/Tests/Adapter/AbstractRedisAdapterTestCase.php
index c83365cc..c139cc97 100644
--- a/Tests/Adapter/AbstractRedisAdapterTestCase.php
+++ b/Tests/Adapter/AbstractRedisAdapterTestCase.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Cache\Tests\Adapter;
use Psr\Cache\CacheItemPoolInterface;
+use Relay\Cluster as RelayCluster;
use Relay\Relay;
use Symfony\Component\Cache\Adapter\RedisAdapter;
@@ -23,7 +24,7 @@ abstract class AbstractRedisAdapterTestCase extends AdapterTestCase
'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
];
- protected static \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
+ protected static \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
{
diff --git a/Tests/Adapter/AdapterTestCase.php b/Tests/Adapter/AdapterTestCase.php
index 031191ca..896ca94a 100644
--- a/Tests/Adapter/AdapterTestCase.php
+++ b/Tests/Adapter/AdapterTestCase.php
@@ -18,6 +18,7 @@
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Contracts\Cache\CallbackInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
abstract class AdapterTestCase extends CachePoolTest
{
@@ -367,6 +368,33 @@ public function testErrorsDontInvalidate()
$this->assertFalse($cache->save($item));
$this->assertSame('bar', $cache->getItem('foo')->get());
}
+
+ public function testNamespaces()
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $cache = $this->createCachePool(0, __FUNCTION__);
+
+ $this->assertInstanceOf(NamespacedPoolInterface::class, $cache);
+
+ $derived = $cache->withSubNamespace('derived');
+
+ $item = $derived->getItem('foo');
+ $derived->save($item->set('Foo'));
+
+ $this->assertFalse($cache->getItem('foo')->isHit());
+
+ $item = $cache->getItem('bar');
+ $cache->save($item->set('Bar'));
+
+ $this->assertFalse($derived->getItem('bar')->isHit());
+ $this->assertTrue($cache->getItem('bar')->isHit());
+
+ $derived = $cache->withSubNamespace('derived');
+ $this->assertTrue($derived->getItem('foo')->isHit());
+ }
}
class NotUnserializable
diff --git a/Tests/Adapter/DoctrineDbalAdapterTest.php b/Tests/Adapter/DoctrineDbalAdapterTest.php
index 79752f39..db20b0f3 100644
--- a/Tests/Adapter/DoctrineDbalAdapterTest.php
+++ b/Tests/Adapter/DoctrineDbalAdapterTest.php
@@ -117,7 +117,7 @@ public function testConfigureSchemaTableExists()
$adapter = new DoctrineDbalAdapter($connection);
$adapter->configureSchema($schema, $connection, fn () => true);
$table = $schema->getTable('cache_items');
- $this->assertEmpty($table->getColumns(), 'The table was not overwritten');
+ $this->assertSame([], $table->getColumns(), 'The table was not overwritten');
}
/**
diff --git a/Tests/Adapter/PhpArrayAdapterTest.php b/Tests/Adapter/PhpArrayAdapterTest.php
index ada3149d..0c856e6f 100644
--- a/Tests/Adapter/PhpArrayAdapterTest.php
+++ b/Tests/Adapter/PhpArrayAdapterTest.php
@@ -58,6 +58,8 @@ class PhpArrayAdapterTest extends AdapterTestCase
'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.',
'testPrune' => 'PhpArrayAdapter just proxies',
+
+ 'testNamespaces' => 'PhpArrayAdapter does not support namespaces.',
];
protected static string $file;
diff --git a/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
index d20ffd55..0f92aee4 100644
--- a/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
+++ b/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php
@@ -28,6 +28,8 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase
'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.',
'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.',
'testPrune' => 'PhpArrayAdapter just proxies',
+
+ 'testNamespaces' => 'PhpArrayAdapter does not support namespaces.',
];
protected static string $file;
diff --git a/Tests/Adapter/PredisAdapterTest.php b/Tests/Adapter/PredisAdapterTest.php
index d9afd85a..730bde71 100644
--- a/Tests/Adapter/PredisAdapterTest.php
+++ b/Tests/Adapter/PredisAdapterTest.php
@@ -42,7 +42,7 @@ public function testCreateConnection()
'scheme' => 'tcp',
'host' => $redisHost[0],
'port' => (int) ($redisHost[1] ?? 6379),
- 'persistent' => 0,
+ 'persistent' => false,
'timeout' => 3,
'read_write_timeout' => 0,
'tcp_nodelay' => true,
@@ -74,7 +74,7 @@ public function testCreateSslConnection()
'host' => $redisHost[0],
'port' => (int) ($redisHost[1] ?? 6379),
'ssl' => ['verify_peer' => '0'],
- 'persistent' => 0,
+ 'persistent' => false,
'timeout' => 3,
'read_write_timeout' => 0,
'tcp_nodelay' => true,
diff --git a/Tests/Adapter/PredisReplicationAdapterTest.php b/Tests/Adapter/PredisReplicationAdapterTest.php
index 28af1b5b..b9877234 100644
--- a/Tests/Adapter/PredisReplicationAdapterTest.php
+++ b/Tests/Adapter/PredisReplicationAdapterTest.php
@@ -27,9 +27,9 @@ public static function setUpBeforeClass(): void
$hosts = explode(' ', getenv('REDIS_REPLICATION_HOSTS'));
$lastArrayKey = array_key_last($hosts);
$hostTable = [];
- foreach($hosts as $key => $host) {
+ foreach ($hosts as $key => $host) {
$hostInformation = array_combine(['host', 'port'], explode(':', $host));
- if($lastArrayKey === $key) {
+ if ($lastArrayKey === $key) {
$hostInformation['role'] = 'master';
}
$hostTable[] = $hostInformation;
diff --git a/Tests/Adapter/RedisAdapterSentinelTest.php b/Tests/Adapter/RedisAdapterSentinelTest.php
index 6dc13b81..9103eec5 100644
--- a/Tests/Adapter/RedisAdapterSentinelTest.php
+++ b/Tests/Adapter/RedisAdapterSentinelTest.php
@@ -32,15 +32,15 @@ public static function setUpBeforeClass(): void
self::markTestSkipped('REDIS_SENTINEL_SERVICE env var is not defined.');
}
- self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['redis_sentinel' => $service, 'prefix' => 'prefix_']);
+ self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['sentinel' => $service, 'prefix' => 'prefix_']);
}
public function testInvalidDSNHasBothClusterAndSentinel()
{
- $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&redis_cluster=1&redis_sentinel=mymaster';
+ $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&cluster=1&sentinel=mymaster';
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
+ $this->expectExceptionMessage('Cannot use both "cluster" and "sentinel" at the same time.');
RedisAdapter::createConnection($dsn);
}
@@ -51,6 +51,6 @@ public function testExceptionMessageWhenFailingToRetrieveMasterInformation()
$dsn = 'redis:?host['.str_replace(' ', ']&host[', $hosts).']';
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Failed to retrieve master information from sentinel "invalid-masterset-name".');
- AbstractAdapter::createConnection($dsn, ['redis_sentinel' => 'invalid-masterset-name']);
+ AbstractAdapter::createConnection($dsn, ['sentinel' => 'invalid-masterset-name']);
}
}
diff --git a/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php b/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php
new file mode 100644
index 00000000..4939d2df
--- /dev/null
+++ b/Tests/Adapter/RedisTagAwareRelayClusterAdapterTest.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
+
+/**
+ * @requires extension relay
+ *
+ * @group integration
+ */
+class RedisTagAwareRelayClusterAdapterTest extends RelayClusterAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
+ {
+ $this->assertInstanceOf(RelayClusterProxy::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/Tests/Adapter/RelayClusterAdapterTest.php b/Tests/Adapter/RelayClusterAdapterTest.php
new file mode 100644
index 00000000..56363f82
--- /dev/null
+++ b/Tests/Adapter/RelayClusterAdapterTest.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use Psr\Cache\CacheItemPoolInterface;
+use Relay\Cluster as RelayCluster;
+use Relay\Relay;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+use Symfony\Component\Cache\Adapter\RedisAdapter;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
+
+/**
+ * @requires extension relay
+ *
+ * @group integration
+ */
+class RelayClusterAdapterTest extends AbstractRedisAdapterTestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ if (!class_exists(RelayCluster::class)) {
+ self::markTestSkipped('The Relay\Cluster class is required.');
+ }
+ if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) {
+ self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.');
+ }
+
+ self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['lazy' => true, 'redis_cluster' => true, 'class' => RelayCluster::class]);
+ self::$redis->setOption(Relay::OPT_PREFIX, 'prefix_');
+ }
+
+ public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
+ {
+ $this->assertInstanceOf(RelayClusterProxy::class, self::$redis);
+ $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+
+ /**
+ * @dataProvider provideFailedCreateConnection
+ */
+ public function testFailedCreateConnection(string $dsn)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Relay cluster connection failed:');
+ RedisAdapter::createConnection($dsn);
+ }
+
+ public static function provideFailedCreateConnection(): array
+ {
+ return [
+ ['redis://localhost:1234?redis_cluster=1&class=Relay\Cluster'],
+ ['redis://foo@localhost?redis_cluster=1&class=Relay\Cluster'],
+ ['redis://localhost/123?redis_cluster=1&class=Relay\Cluster'],
+ ];
+ }
+}
diff --git a/Tests/Adapter/TagAwareTestTrait.php b/Tests/Adapter/TagAwareTestTrait.php
index 8ec1297e..9894ba00 100644
--- a/Tests/Adapter/TagAwareTestTrait.php
+++ b/Tests/Adapter/TagAwareTestTrait.php
@@ -183,4 +183,27 @@ public function testRefreshAfterExpires()
$cacheItem = $pool->getItem('test');
$this->assertTrue($cacheItem->isHit());
}
+
+ public function testNamespacesAndTags()
+ {
+ $pool = $this->createCachePool();
+ $pool->clear();
+
+ $item = $pool->getItem('foo');
+ $item->tag('baz');
+ $pool->save($item);
+
+ $derived = $pool->withSubNamespace('derived');
+ $item = $derived->getItem('bar');
+ $item->tag('baz');
+ $derived->save($item);
+
+ $this->assertTrue($pool->getItem('foo')->isHit());
+ $this->assertTrue($derived->getItem('bar')->isHit());
+
+ $pool->invalidateTags(['baz']);
+
+ $this->assertFalse($pool->getItem('foo')->isHit());
+ $this->assertFalse($derived->getItem('bar')->isHit());
+ }
}
diff --git a/Tests/Psr16CacheProxyTest.php b/Tests/Psr16CacheProxyTest.php
index c3d2d8d5..fa771cf9 100644
--- a/Tests/Psr16CacheProxyTest.php
+++ b/Tests/Psr16CacheProxyTest.php
@@ -45,12 +45,12 @@ public function createSimpleCache(int $defaultLifetime = 0): CacheInterface
public function testProxy()
{
$pool = new ArrayAdapter();
- $cache = new Psr16Cache(new ProxyAdapter($pool, 'my-namespace.'));
+ $cache = new Psr16Cache(new ProxyAdapter($pool, 'my-namespace'));
$this->assertNull($cache->get('some-key'));
$this->assertTrue($cache->set('some-other-key', 'value'));
- $item = $pool->getItem('my-namespace.some-other-key', 'value');
+ $item = $pool->withSubNamespace('my-namespace')->getItem('some-other-key', 'value');
$this->assertTrue($item->isHit());
$this->assertSame('value', $item->get());
}
diff --git a/Tests/Traits/RedisProxiesTest.php b/Tests/Traits/RedisProxiesTest.php
index 1e17b474..50f784da 100644
--- a/Tests/Traits/RedisProxiesTest.php
+++ b/Tests/Traits/RedisProxiesTest.php
@@ -12,10 +12,11 @@
namespace Symfony\Component\Cache\Tests\Traits;
use PHPUnit\Framework\TestCase;
+use Relay\Cluster as RelayCluster;
use Relay\Relay;
use Symfony\Component\Cache\Traits\RedisProxyTrait;
+use Symfony\Component\Cache\Traits\RelayClusterProxy;
use Symfony\Component\Cache\Traits\RelayProxy;
-use Symfony\Component\VarExporter\LazyProxyTrait;
use Symfony\Component\VarExporter\ProxyHelper;
class RedisProxiesTest extends TestCase
@@ -34,8 +35,8 @@ public function testRedisProxy($class)
$expected = substr($proxy, 0, 2 + strpos($proxy, '}'));
$methods = [];
- foreach ((new \ReflectionClass(sprintf('Symfony\Component\Cache\Traits\\%s%dProxy', $class, $version)))->getMethods() as $method) {
- if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name)) {
+ foreach ((new \ReflectionClass(\sprintf('Symfony\Component\Cache\Traits\\%s%dProxy', $class, $version)))->getMethods() as $method) {
+ if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name)) {
continue;
}
$return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
@@ -87,7 +88,7 @@ public function testRelayProxy()
$expectedMethods = [];
foreach ((new \ReflectionClass(RelayProxy::class))->getMethods() as $method) {
- if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name) || $method->isStatic()) {
+ if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
continue;
}
@@ -121,4 +122,51 @@ public function testRelayProxy()
$this->assertEquals($expectedProxy, $proxy);
}
+
+ /**
+ * @requires extension relay
+ */
+ public function testRelayClusterProxy()
+ {
+ $proxy = file_get_contents(\dirname(__DIR__, 2).'/Traits/RelayClusterProxy.php');
+ $proxy = substr($proxy, 0, 2 + strpos($proxy, '}'));
+ $expectedProxy = $proxy;
+ $methods = [];
+ $expectedMethods = [];
+
+ foreach ((new \ReflectionClass(RelayClusterProxy::class))->getMethods() as $method) {
+ if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
+ continue;
+ }
+
+ $return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
+ $expectedMethods[$method->name] = "\n ".ProxyHelper::exportSignature($method, false, $args)."\n".<<initializeLazyObject()->{$method->name}({$args});
+ }
+
+ EOPHP;
+ }
+
+ foreach ((new \ReflectionClass(RelayCluster::class))->getMethods() as $method) {
+ if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
+ continue;
+ }
+ $return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
+ $methods[$method->name] = "\n ".ProxyHelper::exportSignature($method, false, $args)."\n".<<initializeLazyObject()->{$method->name}({$args});
+ }
+
+ EOPHP;
+ }
+
+ uksort($methods, 'strnatcmp');
+ $proxy .= implode('', $methods)."}\n";
+
+ uksort($expectedMethods, 'strnatcmp');
+ $expectedProxy .= implode('', $expectedMethods)."}\n";
+
+ $this->assertEquals($expectedProxy, $proxy);
+ }
}
diff --git a/Traits/AbstractAdapterTrait.php b/Traits/AbstractAdapterTrait.php
index 6a716743..ac8dc97a 100644
--- a/Traits/AbstractAdapterTrait.php
+++ b/Traits/AbstractAdapterTrait.php
@@ -35,6 +35,7 @@ trait AbstractAdapterTrait
*/
private static \Closure $mergeByLifetime;
+ private readonly string $rootNamespace;
private string $namespace = '';
private int $defaultLifetime;
private string $namespaceVersion = '';
@@ -106,15 +107,16 @@ public function clear(string $prefix = ''): bool
{
$this->deferred = [];
if ($cleared = $this->versioningIsEnabled) {
+ $rootNamespace = $this->rootNamespace ??= $this->namespace;
if ('' === $namespaceVersionToClear = $this->namespaceVersion) {
- foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
+ foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
$namespaceVersionToClear = $v;
}
}
- $namespaceToClear = $this->namespace.$namespaceVersionToClear;
+ $namespaceToClear = $rootNamespace.$namespaceVersionToClear;
$namespaceVersion = self::formatNamespaceVersion(mt_rand());
try {
- $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0);
+ $e = $this->doSave([static::NS_SEPARATOR.$rootNamespace => $namespaceVersion], 0);
} catch (\Exception $e) {
}
if (true !== $e && [] !== $e) {
@@ -247,6 +249,16 @@ public function saveDeferred(CacheItemInterface $item): bool
return true;
}
+ public function withSubNamespace(string $namespace): static
+ {
+ $this->rootNamespace ??= $this->namespace;
+
+ $clone = clone $this;
+ $clone->namespace .= CacheItem::validateKey($namespace).static::NS_SEPARATOR;
+
+ return $clone;
+ }
+
/**
* Enables/disables versioning of items.
*
@@ -318,19 +330,24 @@ private function generateItems(iterable $items, array &$keys): \Generator
/**
* @internal
*/
- protected function getId(mixed $key): string
+ protected function getId(mixed $key, ?string $namespace = null): string
{
- if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
+ $namespace ??= $this->namespace;
+
+ if ('' !== $this->namespaceVersion) {
+ $namespace .= $this->namespaceVersion;
+ } elseif ($this->versioningIsEnabled) {
+ $rootNamespace = $this->rootNamespace ??= $this->namespace;
$this->ids = [];
$this->namespaceVersion = '1'.static::NS_SEPARATOR;
try {
- foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
+ foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
$this->namespaceVersion = $v;
}
$e = true;
if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
$this->namespaceVersion = self::formatNamespaceVersion(time());
- $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0);
+ $e = $this->doSave([static::NS_SEPARATOR.$rootNamespace => $this->namespaceVersion], 0);
}
} catch (\Exception $e) {
}
@@ -338,25 +355,34 @@ protected function getId(mixed $key): string
$message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
}
+
+ $namespace .= $this->namespaceVersion;
}
if (\is_string($key) && isset($this->ids[$key])) {
- return $this->namespace.$this->namespaceVersion.$this->ids[$key];
- }
- \assert('' !== CacheItem::validateKey($key));
- $this->ids[$key] = $key;
+ $id = $this->ids[$key];
+ } else {
+ \assert('' !== CacheItem::validateKey($key));
+ $this->ids[$key] = $key;
- if (\count($this->ids) > 1000) {
- $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
- }
+ if (\count($this->ids) > 1000) {
+ $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
+ }
+
+ if (null === $this->maxIdLength) {
+ return $namespace.$key;
+ }
+ if (\strlen($id = $namespace.$key) <= $this->maxIdLength) {
+ return $id;
+ }
- if (null === $this->maxIdLength) {
- return $this->namespace.$this->namespaceVersion.$key;
- }
- if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
// Use xxh128 to favor speed over security, which is not an issue here
$this->ids[$key] = $id = substr_replace(base64_encode(hash('xxh128', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
- $id = $this->namespace.$this->namespaceVersion.$id;
+ }
+ $id = $namespace.$id;
+
+ if (null !== $this->maxIdLength && \strlen($id) > $this->maxIdLength) {
+ return base64_encode(hash('xxh128', $id, true));
}
return $id;
diff --git a/Traits/RedisTrait.php b/Traits/RedisTrait.php
index f6bb9bbe..19b4b911 100644
--- a/Traits/RedisTrait.php
+++ b/Traits/RedisTrait.php
@@ -20,6 +20,7 @@
use Predis\Connection\Replication\ReplicationInterface as Predis2ReplicationInterface;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
+use Relay\Cluster as RelayCluster;
use Relay\Relay;
use Relay\Sentinel;
use Symfony\Component\Cache\Exception\CacheException;
@@ -37,23 +38,25 @@ trait RedisTrait
{
private static array $defaultConnectionOptions = [
'class' => null,
- 'persistent' => 0,
+ 'persistent' => false,
'persistent_id' => null,
'timeout' => 30,
'read_timeout' => 0,
'retry_interval' => 0,
'tcp_keepalive' => 0,
'lazy' => null,
- 'redis_cluster' => false,
- 'redis_sentinel' => null,
+ 'cluster' => false,
+ 'cluster_command_timeout' => 0,
+ 'cluster_relay_context' => [],
+ 'sentinel' => null,
'dbindex' => 0,
'failover' => 'none',
'ssl' => null, // see https://php.net/context.ssl
];
- private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
+ private \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
private MarshallerInterface $marshaller;
- private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
+ private function init(\Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
{
parent::__construct($namespace, $defaultLifetime);
@@ -85,15 +88,15 @@ private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInter
*
* @throws InvalidArgumentException when the DSN is invalid
*/
- public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
+ public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay|RelayCluster
{
- if (str_starts_with($dsn, 'redis:')) {
- $scheme = 'redis';
- } elseif (str_starts_with($dsn, 'rediss:')) {
- $scheme = 'rediss';
- } else {
- throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
- }
+ $scheme = match (true) {
+ str_starts_with($dsn, 'redis:') => 'redis',
+ str_starts_with($dsn, 'rediss:') => 'rediss',
+ str_starts_with($dsn, 'valkey:') => 'valkey',
+ str_starts_with($dsn, 'valkeys:') => 'valkeys',
+ default => throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:" nor "valkey[s]:".'),
+ };
if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
@@ -121,7 +124,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$query = $hosts = [];
- $tls = 'rediss' === $scheme;
+ $tls = 'rediss' === $scheme || 'valkeys' === $scheme;
$tcpScheme = $tls ? 'tls' : 'tcp';
if (isset($params['query'])) {
@@ -174,28 +177,42 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$params += $query + $options + self::$defaultConnectionOptions;
- if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {
- throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');
+ $aliases = [
+ 'sentinel_master' => 'sentinel',
+ 'redis_sentinel' => 'sentinel',
+ 'redis_cluster' => 'cluster',
+ ];
+ foreach ($aliases as $alias => $key) {
+ $params[$key] = match (true) {
+ \array_key_exists($key, $query) => $query[$key],
+ \array_key_exists($alias, $query) => $query[$alias],
+ \array_key_exists($key, $options) => $options[$key],
+ \array_key_exists($alias, $options) => $options[$alias],
+ default => $params[$key],
+ };
}
- $params['redis_sentinel'] ??= $params['sentinel_master'] ?? null;
-
- if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
+ if (isset($params['sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException('Redis Sentinel support requires one of: "predis/predis", "ext-redis >= 5.2", "ext-relay".');
}
- if (isset($params['lazy'])) {
- $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
+ foreach (['lazy', 'persistent', 'cluster'] as $option) {
+ if (!\is_bool($params[$option] ?? false)) {
+ $params[$option] = filter_var($params[$option], \FILTER_VALIDATE_BOOLEAN);
+ }
}
- $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
- if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
- throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
+ if ($params['cluster'] && isset($params['sentinel'])) {
+ throw new InvalidArgumentException('Cannot use both "cluster" and "sentinel" at the same time.');
}
$class = $params['class'] ?? match (true) {
- $params['redis_cluster'] => \extension_loaded('redis') ? \RedisCluster::class : \Predis\Client::class,
- isset($params['redis_sentinel']) => match (true) {
+ $params['cluster'] => match (true) {
+ \extension_loaded('redis') => \RedisCluster::class,
+ \extension_loaded('relay') => RelayCluster::class,
+ default => \Predis\Client::class,
+ },
+ isset($params['sentinel']) => match (true) {
\extension_loaded('redis') => \Redis::class,
\extension_loaded('relay') => Relay::class,
default => \Predis\Client::class,
@@ -206,7 +223,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
default => \Predis\Client::class,
};
- if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
+ if (isset($params['sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException(\sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and neither ext-redis >= 5.2 nor ext-relay have been found.', $class));
}
@@ -230,7 +247,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$host = 'tls://'.$host;
}
- if (!isset($params['redis_sentinel'])) {
+ if (!isset($params['sentinel'])) {
break;
}
@@ -256,37 +273,22 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$sentinel = @new $sentinelClass($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
}
- if ($address = @$sentinel->getMasterAddrByName($params['redis_sentinel'])) {
+ if ($address = @$sentinel->getMasterAddrByName($params['sentinel'])) {
[$host, $port] = $address;
}
} catch (\RedisException|\Relay\Exception $redisException) {
}
} while (++$hostIndex < \count($hosts) && !$address);
- if (isset($params['redis_sentinel']) && !$address) {
- throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel']), previous: $redisException ?? null);
+ if (isset($params['sentinel']) && !$address) {
+ throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['sentinel']), previous: $redisException ?? null);
}
try {
$extra = [
- 'stream' => $params['ssl'] ?? null,
- ];
- $booleanStreamOptions = [
- 'allow_self_signed',
- 'capture_peer_cert',
- 'capture_peer_cert_chain',
- 'disable_compression',
- 'SNI_enabled',
- 'verify_peer',
- 'verify_peer_name',
+ 'stream' => self::filterSslOptions($params['ssl'] ?? []) ?: null,
];
- foreach ($extra['stream'] ?? [] as $streamOption => $value) {
- if (\in_array($streamOption, $booleanStreamOptions, true) && \is_string($value)) {
- $extra['stream'][$streamOption] = filter_var($value, \FILTER_VALIDATE_BOOL);
- }
- }
-
if (isset($params['auth'])) {
$extra['auth'] = $params['auth'];
}
@@ -348,6 +350,59 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
$redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
}
+ } elseif (is_a($class, RelayCluster::class, true)) {
+ if (version_compare(phpversion('relay'), '0.10.0', '<')) {
+ throw new InvalidArgumentException('Using RelayCluster is supported from ext-relay 0.10.0 or higher.');
+ }
+
+ $initializer = static function () use ($class, $params, $hosts) {
+ foreach ($hosts as $i => $host) {
+ $hosts[$i] = match ($host['scheme']) {
+ 'tcp' => $host['host'].':'.$host['port'],
+ 'tls' => 'tls://'.$host['host'].':'.$host['port'],
+ default => $host['path'],
+ };
+ }
+
+ try {
+ $context = $params['cluster_relay_context'];
+ $context['stream'] = self::filterSslOptions($params['ssl'] ?? []) ?: null;
+
+ foreach ($context as $name => $value) {
+ match ($name) {
+ 'use-cache', 'client-tracking', 'throw-on-error', 'client-invalidations', 'reply-literal', 'persistent',
+ => $context[$name] = filter_var($value, \FILTER_VALIDATE_BOOLEAN),
+ 'max-retries', 'serializer', 'compression', 'compression-level',
+ => $context[$name] = filter_var($value, \FILTER_VALIDATE_INT),
+ default => null,
+ };
+ }
+
+ $relayCluster = new $class(
+ name: null,
+ seeds: $hosts,
+ connect_timeout: $params['timeout'],
+ command_timeout: $params['cluster_command_timeout'],
+ persistent: $params['persistent'],
+ auth: $params['auth'] ?? null,
+ context: $context,
+ );
+ } catch (\Relay\Exception $e) {
+ throw new InvalidArgumentException('Relay cluster connection failed: '.$e->getMessage());
+ }
+
+ if (0 < $params['tcp_keepalive']) {
+ $relayCluster->setOption(Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
+ }
+
+ if (0 < $params['read_timeout']) {
+ $relayCluster->setOption(Relay::OPT_READ_TIMEOUT, $params['read_timeout']);
+ }
+
+ return $relayCluster;
+ };
+
+ $redis = $params['lazy'] ? RelayClusterProxy::createLazyProxy($initializer) : $initializer();
} elseif (is_a($class, \RedisCluster::class, true)) {
$initializer = static function () use ($isRedisExt, $class, $params, $hosts) {
foreach ($hosts as $i => $host) {
@@ -359,7 +414,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
}
try {
- $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
+ $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
} catch (\RedisClusterException $e) {
throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
}
@@ -379,11 +434,14 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$redis = $params['lazy'] ? RedisClusterProxy::createLazyProxy($initializer) : $initializer();
} elseif (is_a($class, \Predis\ClientInterface::class, true)) {
- if ($params['redis_cluster']) {
+ if ($params['cluster']) {
$params['cluster'] = 'redis';
- } elseif (isset($params['redis_sentinel'])) {
+ } else {
+ unset($params['cluster']);
+ }
+ if (isset($params['sentinel'])) {
$params['replication'] = 'sentinel';
- $params['service'] = $params['redis_sentinel'];
+ $params['service'] = $params['sentinel'];
}
$params += ['parameters' => []];
$params['parameters'] += [
@@ -411,7 +469,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
}
}
- if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
+ if (1 === \count($hosts) && !isset($params['cluster']) & !isset($params['sentinel'])) {
$hosts = $hosts[0];
} elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) {
$params['replication'] = true;
@@ -419,8 +477,8 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
}
$params['exceptions'] = false;
- $redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions));
- if (isset($params['redis_sentinel'])) {
+ $redis = new $class($hosts, array_diff_key($params, array_diff_key(self::$defaultConnectionOptions, ['cluster' => null])));
+ if (isset($params['sentinel'])) {
$redis->getConnection()->setSentinelTimeout($params['timeout']);
}
} elseif (class_exists($class, false)) {
@@ -478,6 +536,35 @@ protected function doClear(string $namespace): bool
}
$cleared = true;
+
+ if ($this->redis instanceof RelayCluster) {
+ $prefix = Relay::SCAN_PREFIX & $this->redis->getOption(Relay::OPT_SCAN) ? '' : $this->redis->getOption(Relay::OPT_PREFIX);
+ $prefixLen = \strlen($prefix);
+ $pattern = $prefix.$namespace.'*';
+ foreach ($this->redis->_masters() as $ipAndPort) {
+ $address = implode(':', $ipAndPort);
+ $cursor = null;
+ do {
+ $keys = $this->redis->scan($cursor, $address, $pattern, 1000);
+ if (isset($keys[1]) && \is_array($keys[1])) {
+ $cursor = $keys[0];
+ $keys = $keys[1];
+ }
+
+ if ($keys) {
+ if ($prefixLen) {
+ foreach ($keys as $i => $key) {
+ $keys[$i] = substr($key, $prefixLen);
+ }
+ }
+ $this->doDelete($keys);
+ }
+ } while ($cursor);
+ }
+
+ return $cleared;
+ }
+
$hosts = $this->getHosts();
$host = reset($hosts);
if ($host instanceof \Predis\Client) {
@@ -605,8 +692,9 @@ private function pipeline(\Closure $generator, ?object $redis = null): \Generato
$ids = [];
$redis ??= $this->redis;
- if ($redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
+ if ($redis instanceof \RedisCluster || $redis instanceof RelayCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
// phpredis & predis don't support pipelining with RedisCluster
+ // \Relay\Cluster does not support multi with pipeline mode
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
// see https://github.com/nrk/predis/issues/267#issuecomment-123781423
$results = [];
@@ -687,4 +775,17 @@ private function getHosts(): array
return $hosts;
}
+
+ private static function filterSslOptions(array $options): array
+ {
+ foreach ($options as $name => $value) {
+ match ($name) {
+ 'allow_self_signed', 'capture_peer_cert', 'capture_peer_cert_chain', 'disable_compression', 'SNI_enabled', 'verify_peer', 'verify_peer_name',
+ => $options[$name] = filter_var($value, \FILTER_VALIDATE_BOOLEAN),
+ default => null,
+ };
+ }
+
+ return $options;
+ }
}
diff --git a/Traits/RelayClusterProxy.php b/Traits/RelayClusterProxy.php
new file mode 100644
index 00000000..fd5f08b5
--- /dev/null
+++ b/Traits/RelayClusterProxy.php
@@ -0,0 +1,1204 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Relay\Cluster;
+use Relay\Relay;
+use Symfony\Component\VarExporter\LazyObjectInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
+class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
+class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
+
+/**
+ * @internal
+ */
+class RelayClusterProxy extends Cluster implements ResetInterface, LazyObjectInterface
+{
+ use RedisProxyTrait {
+ resetLazyObject as reset;
+ }
+
+ public function __construct(
+ string|null $name,
+ array|null $seeds = null,
+ int|float $connect_timeout = 0,
+ int|float $command_timeout = 0,
+ bool $persistent = false,
+ #[\SensitiveParameter] mixed $auth = null,
+ array|null $context = null,
+ ) {
+ $this->initializeLazyObject()->__construct(...\func_get_args());
+ }
+
+ public function close(): bool
+ {
+ return $this->initializeLazyObject()->close(...\func_get_args());
+ }
+
+ public function listen(?callable $callback): bool
+ {
+ return $this->initializeLazyObject()->listen(...\func_get_args());
+ }
+
+ public function onFlushed(?callable $callback): bool
+ {
+ return $this->initializeLazyObject()->onFlushed(...\func_get_args());
+ }
+
+ public function onInvalidated(?callable $callback, ?string $pattern = null): bool
+ {
+ return $this->initializeLazyObject()->onInvalidated(...\func_get_args());
+ }
+
+ public function dispatchEvents(): false|int
+ {
+ return $this->initializeLazyObject()->dispatchEvents(...\func_get_args());
+ }
+
+ public function dump(mixed $key): Cluster|string|false
+ {
+ return $this->initializeLazyObject()->dump(...\func_get_args());
+ }
+
+ public function getOption(int $option): mixed
+ {
+ return $this->initializeLazyObject()->getOption(...\func_get_args());
+ }
+
+ public function setOption(int $option, mixed $value): bool
+ {
+ return $this->initializeLazyObject()->setOption(...\func_get_args());
+ }
+
+ public function getTransferredBytes(): array|false
+ {
+ return $this->initializeLazyObject()->getTransferredBytes(...\func_get_args());
+ }
+
+ public function getrange(mixed $key, int $start, int $end): Cluster|string|false
+ {
+ return $this->initializeLazyObject()->getrange(...\func_get_args());
+ }
+
+ public function addIgnorePatterns(string ...$pattern): int
+ {
+ return $this->initializeLazyObject()->addIgnorePatterns(...\func_get_args());
+ }
+
+ public function addAllowPatterns(string ...$pattern): int
+ {
+ return $this->initializeLazyObject()->addAllowPatterns(...\func_get_args());
+ }
+
+ public function _serialize(mixed $value): string
+ {
+ return $this->initializeLazyObject()->_serialize(...\func_get_args());
+ }
+
+ public function _unserialize(string $value): mixed
+ {
+ return $this->initializeLazyObject()->_unserialize(...\func_get_args());
+ }
+
+ public function _compress(string $value): string
+ {
+ return $this->initializeLazyObject()->_compress(...\func_get_args());
+ }
+
+ public function _uncompress(string $value): string
+ {
+ return $this->initializeLazyObject()->_uncompress(...\func_get_args());
+ }
+
+ public function _pack(mixed $value): string
+ {
+ return $this->initializeLazyObject()->_pack(...\func_get_args());
+ }
+
+ public function _unpack(string $value): mixed
+ {
+ return $this->initializeLazyObject()->_unpack(...\func_get_args());
+ }
+
+ public function _prefix(mixed $value): string
+ {
+ return $this->initializeLazyObject()->_prefix(...\func_get_args());
+ }
+
+ public function getLastError(): ?string
+ {
+ return $this->initializeLazyObject()->getLastError(...\func_get_args());
+ }
+
+ public function clearLastError(): bool
+ {
+ return $this->initializeLazyObject()->clearLastError(...\func_get_args());
+ }
+
+ public function clearTransferredBytes(): bool
+ {
+ return $this->initializeLazyObject()->clearTransferredBytes(...\func_get_args());
+ }
+
+ public function endpointId(): array|false
+ {
+ return $this->initializeLazyObject()->endpointId(...\func_get_args());
+ }
+
+ public function rawCommand(array|string $key_or_address, string $cmd, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->rawCommand(...\func_get_args());
+ }
+
+ public function cluster(array|string $key_or_address, string $operation, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->cluster(...\func_get_args());
+ }
+
+ public function info(array|string $key_or_address, string ...$sections): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->info(...\func_get_args());
+ }
+
+ public function flushdb(array|string $key_or_address, bool|null $sync = null): Cluster|bool
+ {
+ return $this->initializeLazyObject()->flushdb(...\func_get_args());
+ }
+
+ public function flushall(array|string $key_or_address, bool|null $sync = null): Cluster|bool
+ {
+ return $this->initializeLazyObject()->flushall(...\func_get_args());
+ }
+
+ public function dbsize(array|string $key_or_address): Cluster|int|false
+ {
+ return $this->initializeLazyObject()->dbsize(...\func_get_args());
+ }
+
+ public function waitaof(array|string $key_or_address, int $numlocal, int $numremote, int $timeout): Relay|array|false
+ {
+ return $this->initializeLazyObject()->waitaof(...\func_get_args());
+ }
+
+ public function restore(mixed $key, int $ttl, string $value, array|null $options = null): Cluster|bool
+ {
+ return $this->initializeLazyObject()->restore(...\func_get_args());
+ }
+
+ public function echo(array|string $key_or_address, string $message): Cluster|string|false
+ {
+ return $this->initializeLazyObject()->echo(...\func_get_args());
+ }
+
+ public function ping(array|string $key_or_address, string|null $message = null): Cluster|bool|string
+ {
+ return $this->initializeLazyObject()->ping(...\func_get_args());
+ }
+
+ public function idleTime(): int
+ {
+ return $this->initializeLazyObject()->idleTime(...\func_get_args());
+ }
+
+ public function randomkey(array|string $key_or_address): Cluster|bool|string
+ {
+ return $this->initializeLazyObject()->randomkey(...\func_get_args());
+ }
+
+ public function time(array|string $key_or_address): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->time(...\func_get_args());
+ }
+
+ public function bgrewriteaof(array|string $key_or_address): Cluster|bool
+ {
+ return $this->initializeLazyObject()->bgrewriteaof(...\func_get_args());
+ }
+
+ public function lastsave(array|string $key_or_address): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->lastsave(...\func_get_args());
+ }
+
+ public function lcs(mixed $key1, mixed $key2, array|null $options = null): mixed
+ {
+ return $this->initializeLazyObject()->lcs(...\func_get_args());
+ }
+
+ public function bgsave(array|string $key_or_address, bool $schedule = false): Cluster|bool
+ {
+ return $this->initializeLazyObject()->bgsave(...\func_get_args());
+ }
+
+ public function save(array|string $key_or_address): Cluster|bool
+ {
+ return $this->initializeLazyObject()->save(...\func_get_args());
+ }
+
+ public function role(array|string $key_or_address): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->role(...\func_get_args());
+ }
+
+ public function ttl(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->ttl(...\func_get_args());
+ }
+
+ public function pttl(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->pttl(...\func_get_args());
+ }
+
+ public function exists(mixed ...$keys): Cluster|bool|int
+ {
+ return $this->initializeLazyObject()->exists(...\func_get_args());
+ }
+
+ public function eval(mixed $script, array $args = [], int $num_keys = 0): mixed
+ {
+ return $this->initializeLazyObject()->eval(...\func_get_args());
+ }
+
+ public function eval_ro(mixed $script, array $args = [], int $num_keys = 0): mixed
+ {
+ return $this->initializeLazyObject()->eval_ro(...\func_get_args());
+ }
+
+ public function evalsha(string $sha, array $args = [], int $num_keys = 0): mixed
+ {
+ return $this->initializeLazyObject()->evalsha(...\func_get_args());
+ }
+
+ public function evalsha_ro(string $sha, array $args = [], int $num_keys = 0): mixed
+ {
+ return $this->initializeLazyObject()->evalsha_ro(...\func_get_args());
+ }
+
+ public function client(array|string $key_or_address, string $operation, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->client(...\func_get_args());
+ }
+
+ public function geoadd(mixed $key, float $lng, float $lat, string $member, mixed ...$other_triples_and_options): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->geoadd(...\func_get_args());
+ }
+
+ public function geodist(mixed $key, string $src, string $dst, string|null $unit = null): Cluster|float|false
+ {
+ return $this->initializeLazyObject()->geodist(...\func_get_args());
+ }
+
+ public function geohash(mixed $key, string $member, string ...$other_members): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->geohash(...\func_get_args());
+ }
+
+ public function georadius(mixed $key, float $lng, float $lat, float $radius, string $unit, array $options = []): mixed
+ {
+ return $this->initializeLazyObject()->georadius(...\func_get_args());
+ }
+
+ public function georadiusbymember(mixed $key, string $member, float $radius, string $unit, array $options = []): mixed
+ {
+ return $this->initializeLazyObject()->georadiusbymember(...\func_get_args());
+ }
+
+ public function georadiusbymember_ro(mixed $key, string $member, float $radius, string $unit, array $options = []): mixed
+ {
+ return $this->initializeLazyObject()->georadiusbymember_ro(...\func_get_args());
+ }
+
+ public function georadius_ro(mixed $key, float $lng, float $lat, float $radius, string $unit, array $options = []): mixed
+ {
+ return $this->initializeLazyObject()->georadius_ro(...\func_get_args());
+ }
+
+ public function geosearchstore(mixed $dstkey, mixed $srckey, array|string $position, array|int|float $shape, string $unit, array $options = []): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->geosearchstore(...\func_get_args());
+ }
+
+ public function geosearch(mixed $key, array|string $position, array|int|float $shape, string $unit, array $options = []): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->geosearch(...\func_get_args());
+ }
+
+ public function get(mixed $key): mixed
+ {
+ return $this->initializeLazyObject()->get(...\func_get_args());
+ }
+
+ public function getset(mixed $key, mixed $value): mixed
+ {
+ return $this->initializeLazyObject()->getset(...\func_get_args());
+ }
+
+ public function setrange(mixed $key, int $start, mixed $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->setrange(...\func_get_args());
+ }
+
+ public function getbit(mixed $key, int $pos): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->getbit(...\func_get_args());
+ }
+
+ public function bitcount(mixed $key, int $start = 0, int $end = -1, bool $by_bit = false): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->bitcount(...\func_get_args());
+ }
+
+ public function config(array|string $key_or_address, string $operation, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->config(...\func_get_args());
+ }
+
+ public function command(mixed ...$args): Cluster|array|false|int
+ {
+ return $this->initializeLazyObject()->command(...\func_get_args());
+ }
+
+ public function bitop(string $operation, string $dstkey, string $srckey, string ...$other_keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->bitop(...\func_get_args());
+ }
+
+ public function bitpos(mixed $key, int $bit, ?int $start = null, ?int $end = null, bool $by_bit = false): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->bitpos(...\func_get_args());
+ }
+
+ public function blmove(mixed $srckey, mixed $dstkey, string $srcpos, string $dstpos, float $timeout): Cluster|string|false|null
+ {
+ return $this->initializeLazyObject()->blmove(...\func_get_args());
+ }
+
+ public function lmove(mixed $srckey, mixed $dstkey, string $srcpos, string $dstpos): Cluster|string|false|null
+ {
+ return $this->initializeLazyObject()->lmove(...\func_get_args());
+ }
+
+ public function setbit(mixed $key, int $pos, int $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->setbit(...\func_get_args());
+ }
+
+ public function acl(array|string $key_or_address, string $operation, string ...$args): mixed
+ {
+ return $this->initializeLazyObject()->acl(...\func_get_args());
+ }
+
+ public function append(mixed $key, mixed $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->append(...\func_get_args());
+ }
+
+ public function set(mixed $key, mixed $value, mixed $options = null): Cluster|string|bool
+ {
+ return $this->initializeLazyObject()->set(...\func_get_args());
+ }
+
+ public function getex(mixed $key, ?array $options = null): mixed
+ {
+ return $this->initializeLazyObject()->getex(...\func_get_args());
+ }
+
+ public function setex(mixed $key, int $seconds, mixed $value): Cluster|bool
+ {
+ return $this->initializeLazyObject()->setex(...\func_get_args());
+ }
+
+ public function pfadd(mixed $key, array $elements): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->pfadd(...\func_get_args());
+ }
+
+ public function pfcount(mixed $key): Cluster|int|false
+ {
+ return $this->initializeLazyObject()->pfcount(...\func_get_args());
+ }
+
+ public function pfmerge(string $dstkey, array $srckeys): Cluster|bool
+ {
+ return $this->initializeLazyObject()->pfmerge(...\func_get_args());
+ }
+
+ public function psetex(mixed $key, int $milliseconds, mixed $value): Cluster|bool
+ {
+ return $this->initializeLazyObject()->psetex(...\func_get_args());
+ }
+
+ public function publish(string $channel, string $message): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->publish(...\func_get_args());
+ }
+
+ public function pubsub(array|string $key_or_address, string $operation, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->pubsub(...\func_get_args());
+ }
+
+ public function setnx(mixed $key, mixed $value): Cluster|bool
+ {
+ return $this->initializeLazyObject()->setnx(...\func_get_args());
+ }
+
+ public function mget(array $keys): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->mget(...\func_get_args());
+ }
+
+ public function mset(array $kvals): Cluster|array|bool
+ {
+ return $this->initializeLazyObject()->mset(...\func_get_args());
+ }
+
+ public function msetnx(array $kvals): Cluster|array|bool
+ {
+ return $this->initializeLazyObject()->msetnx(...\func_get_args());
+ }
+
+ public function rename(mixed $key, mixed $newkey): Cluster|bool
+ {
+ return $this->initializeLazyObject()->rename(...\func_get_args());
+ }
+
+ public function renamenx(mixed $key, mixed $newkey): Cluster|bool
+ {
+ return $this->initializeLazyObject()->renamenx(...\func_get_args());
+ }
+
+ public function del(mixed ...$keys): Cluster|bool|int
+ {
+ return $this->initializeLazyObject()->del(...\func_get_args());
+ }
+
+ public function unlink(mixed ...$keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->unlink(...\func_get_args());
+ }
+
+ public function expire(mixed $key, int $seconds, string|null $mode = null): Cluster|bool
+ {
+ return $this->initializeLazyObject()->expire(...\func_get_args());
+ }
+
+ public function pexpire(mixed $key, int $milliseconds): Cluster|bool
+ {
+ return $this->initializeLazyObject()->pexpire(...\func_get_args());
+ }
+
+ public function expireat(mixed $key, int $timestamp): Cluster|bool
+ {
+ return $this->initializeLazyObject()->expireat(...\func_get_args());
+ }
+
+ public function expiretime(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->expiretime(...\func_get_args());
+ }
+
+ public function pexpireat(mixed $key, int $timestamp_ms): Cluster|bool
+ {
+ return $this->initializeLazyObject()->pexpireat(...\func_get_args());
+ }
+
+ public static function flushMemory(?string $endpointId = null, ?int $db = null): bool
+ {
+ return Cluster::flushMemory(...\func_get_args());
+ }
+
+ public function pexpiretime(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->pexpiretime(...\func_get_args());
+ }
+
+ public function persist(mixed $key): Cluster|bool
+ {
+ return $this->initializeLazyObject()->persist(...\func_get_args());
+ }
+
+ public function type(mixed $key): Cluster|bool|int|string
+ {
+ return $this->initializeLazyObject()->type(...\func_get_args());
+ }
+
+ public function lrange(mixed $key, int $start, int $stop): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->lrange(...\func_get_args());
+ }
+
+ public function lpush(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->lpush(...\func_get_args());
+ }
+
+ public function rpush(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->rpush(...\func_get_args());
+ }
+
+ public function lpushx(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->lpushx(...\func_get_args());
+ }
+
+ public function rpushx(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->rpushx(...\func_get_args());
+ }
+
+ public function lset(mixed $key, int $index, mixed $member): Cluster|bool
+ {
+ return $this->initializeLazyObject()->lset(...\func_get_args());
+ }
+
+ public function lpop(mixed $key, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->lpop(...\func_get_args());
+ }
+
+ public function lpos(mixed $key, mixed $value, array|null $options = null): mixed
+ {
+ return $this->initializeLazyObject()->lpos(...\func_get_args());
+ }
+
+ public function rpop(mixed $key, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->rpop(...\func_get_args());
+ }
+
+ public function rpoplpush(mixed $srckey, mixed $dstkey): mixed
+ {
+ return $this->initializeLazyObject()->rpoplpush(...\func_get_args());
+ }
+
+ public function brpoplpush(mixed $srckey, mixed $dstkey, float $timeout): mixed
+ {
+ return $this->initializeLazyObject()->brpoplpush(...\func_get_args());
+ }
+
+ public function blpop(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->blpop(...\func_get_args());
+ }
+
+ public function blmpop(float $timeout, array $keys, string $from, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->blmpop(...\func_get_args());
+ }
+
+ public function bzmpop(float $timeout, array $keys, string $from, int $count = 1): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->bzmpop(...\func_get_args());
+ }
+
+ public function lmpop(array $keys, string $from, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->lmpop(...\func_get_args());
+ }
+
+ public function zmpop(array $keys, string $from, int $count = 1): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->zmpop(...\func_get_args());
+ }
+
+ public function brpop(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->brpop(...\func_get_args());
+ }
+
+ public function bzpopmax(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->bzpopmax(...\func_get_args());
+ }
+
+ public function bzpopmin(string|array $key, string|float $timeout_or_key, mixed ...$extra_args): Cluster|array|false|null
+ {
+ return $this->initializeLazyObject()->bzpopmin(...\func_get_args());
+ }
+
+ public function object(string $op, mixed $key): mixed
+ {
+ return $this->initializeLazyObject()->object(...\func_get_args());
+ }
+
+ public function geopos(mixed $key, mixed ...$members): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->geopos(...\func_get_args());
+ }
+
+ public function lrem(mixed $key, mixed $member, int $count = 0): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->lrem(...\func_get_args());
+ }
+
+ public function lindex(mixed $key, int $index): mixed
+ {
+ return $this->initializeLazyObject()->lindex(...\func_get_args());
+ }
+
+ public function linsert(mixed $key, string $op, mixed $pivot, mixed $element): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->linsert(...\func_get_args());
+ }
+
+ public function ltrim(mixed $key, int $start, int $end): Cluster|bool
+ {
+ return $this->initializeLazyObject()->ltrim(...\func_get_args());
+ }
+
+ public static function maxMemory(): int
+ {
+ return Cluster::maxMemory();
+ }
+
+ public function hget(mixed $key, mixed $member): mixed
+ {
+ return $this->initializeLazyObject()->hget(...\func_get_args());
+ }
+
+ public function hstrlen(mixed $key, mixed $member): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->hstrlen(...\func_get_args());
+ }
+
+ public function hgetall(mixed $key): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->hgetall(...\func_get_args());
+ }
+
+ public function hkeys(mixed $key): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->hkeys(...\func_get_args());
+ }
+
+ public function hvals(mixed $key): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->hvals(...\func_get_args());
+ }
+
+ public function hmget(mixed $key, array $members): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->hmget(...\func_get_args());
+ }
+
+ public function hmset(mixed $key, array $members): Cluster|bool
+ {
+ return $this->initializeLazyObject()->hmset(...\func_get_args());
+ }
+
+ public function hexists(mixed $key, mixed $member): Cluster|bool
+ {
+ return $this->initializeLazyObject()->hexists(...\func_get_args());
+ }
+
+ public function hrandfield(mixed $key, array|null $options = null): Cluster|array|string|false
+ {
+ return $this->initializeLazyObject()->hrandfield(...\func_get_args());
+ }
+
+ public function hsetnx(mixed $key, mixed $member, mixed $value): Cluster|bool
+ {
+ return $this->initializeLazyObject()->hsetnx(...\func_get_args());
+ }
+
+ public function hset(mixed $key, mixed ...$keys_and_vals): Cluster|int|false
+ {
+ return $this->initializeLazyObject()->hset(...\func_get_args());
+ }
+
+ public function hdel(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->hdel(...\func_get_args());
+ }
+
+ public function hincrby(mixed $key, mixed $member, int $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->hincrby(...\func_get_args());
+ }
+
+ public function hincrbyfloat(mixed $key, mixed $member, float $value): Cluster|bool|float
+ {
+ return $this->initializeLazyObject()->hincrbyfloat(...\func_get_args());
+ }
+
+ public function incr(mixed $key, int $by = 1): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->incr(...\func_get_args());
+ }
+
+ public function decr(mixed $key, int $by = 1): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->decr(...\func_get_args());
+ }
+
+ public function incrby(mixed $key, int $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->incrby(...\func_get_args());
+ }
+
+ public function decrby(mixed $key, int $value): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->decrby(...\func_get_args());
+ }
+
+ public function incrbyfloat(mixed $key, float $value): Cluster|false|float
+ {
+ return $this->initializeLazyObject()->incrbyfloat(...\func_get_args());
+ }
+
+ public function sdiff(mixed $key, mixed ...$other_keys): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->sdiff(...\func_get_args());
+ }
+
+ public function sdiffstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->sdiffstore(...\func_get_args());
+ }
+
+ public function sinter(mixed $key, mixed ...$other_keys): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->sinter(...\func_get_args());
+ }
+
+ public function sintercard(array $keys, int $limit = -1): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->sintercard(...\func_get_args());
+ }
+
+ public function sinterstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->sinterstore(...\func_get_args());
+ }
+
+ public function sunion(mixed $key, mixed ...$other_keys): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->sunion(...\func_get_args());
+ }
+
+ public function sunionstore(mixed $key, mixed ...$other_keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->sunionstore(...\func_get_args());
+ }
+
+ public function subscribe(array $channels, callable $callback): bool
+ {
+ return $this->initializeLazyObject()->subscribe(...\func_get_args());
+ }
+
+ public function unsubscribe(array $channels = []): bool
+ {
+ return $this->initializeLazyObject()->unsubscribe(...\func_get_args());
+ }
+
+ public function psubscribe(array $patterns, callable $callback): bool
+ {
+ return $this->initializeLazyObject()->psubscribe(...\func_get_args());
+ }
+
+ public function punsubscribe(array $patterns = []): bool
+ {
+ return $this->initializeLazyObject()->punsubscribe(...\func_get_args());
+ }
+
+ public function ssubscribe(array $channels, callable $callback): bool
+ {
+ return $this->initializeLazyObject()->ssubscribe(...\func_get_args());
+ }
+
+ public function sunsubscribe(array $channels = []): bool
+ {
+ return $this->initializeLazyObject()->sunsubscribe(...\func_get_args());
+ }
+
+ public function touch(array|string $key_or_array, mixed ...$more_keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->touch(...\func_get_args());
+ }
+
+ public function multi(int $mode = Relay::MULTI): Cluster|bool
+ {
+ return $this->initializeLazyObject()->multi(...\func_get_args());
+ }
+
+ public function exec(): array|false
+ {
+ return $this->initializeLazyObject()->exec(...\func_get_args());
+ }
+
+ public function watch(mixed $key, mixed ...$other_keys): Cluster|bool
+ {
+ return $this->initializeLazyObject()->watch(...\func_get_args());
+ }
+
+ public function unwatch(): Cluster|bool
+ {
+ return $this->initializeLazyObject()->unwatch(...\func_get_args());
+ }
+
+ public function discard(): bool
+ {
+ return $this->initializeLazyObject()->discard(...\func_get_args());
+ }
+
+ public function getMode(bool $masked = false): int
+ {
+ return $this->initializeLazyObject()->getMode(...\func_get_args());
+ }
+
+ public function scan(mixed &$iterator, array|string $key_or_address, mixed $match = null, int $count = 0, string|null $type = null): array|false
+ {
+ return $this->initializeLazyObject()->scan($iterator, ...\array_slice(\func_get_args(), 1));
+ }
+
+ public function hscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+ {
+ return $this->initializeLazyObject()->hscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+ }
+
+ public function sscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+ {
+ return $this->initializeLazyObject()->sscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+ }
+
+ public function zscan(mixed $key, mixed &$iterator, mixed $match = null, int $count = 0): array|false
+ {
+ return $this->initializeLazyObject()->zscan($key, $iterator, ...\array_slice(\func_get_args(), 2));
+ }
+
+ public function zscore(mixed $key, mixed $member): Cluster|float|false
+ {
+ return $this->initializeLazyObject()->zscore(...\func_get_args());
+ }
+
+ public function keys(mixed $pattern): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->keys(...\func_get_args());
+ }
+
+ public function slowlog(array|string $key_or_address, string $operation, mixed ...$args): Cluster|array|bool|int
+ {
+ return $this->initializeLazyObject()->slowlog(...\func_get_args());
+ }
+
+ public function xadd(mixed $key, string $id, array $values, int $maxlen = 0, bool $approx = false, bool $nomkstream = false): Cluster|string|false
+ {
+ return $this->initializeLazyObject()->xadd(...\func_get_args());
+ }
+
+ public function smembers(mixed $key): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->smembers(...\func_get_args());
+ }
+
+ public function sismember(mixed $key, mixed $member): Cluster|bool
+ {
+ return $this->initializeLazyObject()->sismember(...\func_get_args());
+ }
+
+ public function smismember(mixed $key, mixed ...$members): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->smismember(...\func_get_args());
+ }
+
+ public function srem(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->srem(...\func_get_args());
+ }
+
+ public function sadd(mixed $key, mixed $member, mixed ...$members): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->sadd(...\func_get_args());
+ }
+
+ public function sort(mixed $key, array $options = []): Cluster|array|false|int
+ {
+ return $this->initializeLazyObject()->sort(...\func_get_args());
+ }
+
+ public function sort_ro(mixed $key, array $options = []): Cluster|array|false|int
+ {
+ return $this->initializeLazyObject()->sort_ro(...\func_get_args());
+ }
+
+ public function smove(mixed $srckey, mixed $dstkey, mixed $member): Cluster|bool
+ {
+ return $this->initializeLazyObject()->smove(...\func_get_args());
+ }
+
+ public function spop(mixed $key, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->spop(...\func_get_args());
+ }
+
+ public function srandmember(mixed $key, int $count = 1): mixed
+ {
+ return $this->initializeLazyObject()->srandmember(...\func_get_args());
+ }
+
+ public function scard(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->scard(...\func_get_args());
+ }
+
+ public function script(array|string $key_or_address, string $operation, string ...$args): mixed
+ {
+ return $this->initializeLazyObject()->script(...\func_get_args());
+ }
+
+ public function strlen(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->strlen(...\func_get_args());
+ }
+
+ public function hlen(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->hlen(...\func_get_args());
+ }
+
+ public function llen(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->llen(...\func_get_args());
+ }
+
+ public function xack(mixed $key, string $group, array $ids): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->xack(...\func_get_args());
+ }
+
+ public function xclaim(mixed $key, string $group, string $consumer, int $min_idle, array $ids, array $options): Cluster|array|bool
+ {
+ return $this->initializeLazyObject()->xclaim(...\func_get_args());
+ }
+
+ public function xautoclaim(mixed $key, string $group, string $consumer, int $min_idle, string $start, int $count = -1, bool $justid = false): Cluster|array|bool
+ {
+ return $this->initializeLazyObject()->xautoclaim(...\func_get_args());
+ }
+
+ public function xlen(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->xlen(...\func_get_args());
+ }
+
+ public function xgroup(string $operation, mixed $key = null, ?string $group = null, ?string $id_or_consumer = null, bool $mkstream = false, int $entries_read = -2): mixed
+ {
+ return $this->initializeLazyObject()->xgroup(...\func_get_args());
+ }
+
+ public function xdel(mixed $key, array $ids): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->xdel(...\func_get_args());
+ }
+
+ public function xinfo(string $operation, string|null $arg1 = null, string|null $arg2 = null, int $count = -1): mixed
+ {
+ return $this->initializeLazyObject()->xinfo(...\func_get_args());
+ }
+
+ public function xpending(mixed $key, string $group, string|null $start = null, string|null $end = null, int $count = -1, string|null $consumer = null, int $idle = 0): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->xpending(...\func_get_args());
+ }
+
+ public function xrange(mixed $key, string $start, string $end, int $count = -1): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->xrange(...\func_get_args());
+ }
+
+ public function xread(array $streams, int $count = -1, int $block = -1): Cluster|array|bool|null
+ {
+ return $this->initializeLazyObject()->xread(...\func_get_args());
+ }
+
+ public function xreadgroup(mixed $key, string $consumer, array $streams, int $count = 1, int $block = 1): Cluster|array|bool|null
+ {
+ return $this->initializeLazyObject()->xreadgroup(...\func_get_args());
+ }
+
+ public function xrevrange(mixed $key, string $end, string $start, int $count = -1): Cluster|array|bool
+ {
+ return $this->initializeLazyObject()->xrevrange(...\func_get_args());
+ }
+
+ public function xtrim(mixed $key, string $threshold, bool $approx = false, bool $minid = false, int $limit = -1): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->xtrim(...\func_get_args());
+ }
+
+ public function zadd(mixed $key, mixed ...$args): mixed
+ {
+ return $this->initializeLazyObject()->zadd(...\func_get_args());
+ }
+
+ public function zrandmember(mixed $key, array|null $options = null): mixed
+ {
+ return $this->initializeLazyObject()->zrandmember(...\func_get_args());
+ }
+
+ public function zrange(mixed $key, string $start, string $end, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrange(...\func_get_args());
+ }
+
+ public function zrevrange(mixed $key, int $start, int $end, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrevrange(...\func_get_args());
+ }
+
+ public function zrangebyscore(mixed $key, mixed $start, mixed $end, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrangebyscore(...\func_get_args());
+ }
+
+ public function zrevrangebyscore(mixed $key, mixed $start, mixed $end, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrevrangebyscore(...\func_get_args());
+ }
+
+ public function zrevrank(mixed $key, mixed $rank, bool $withscore = false): Cluster|array|int|false
+ {
+ return $this->initializeLazyObject()->zrevrank(...\func_get_args());
+ }
+
+ public function zrangestore(mixed $dstkey, mixed $srckey, mixed $start, mixed $end, mixed $options = null): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zrangestore(...\func_get_args());
+ }
+
+ public function zrank(mixed $key, mixed $rank, bool $withscore = false): Cluster|array|int|false
+ {
+ return $this->initializeLazyObject()->zrank(...\func_get_args());
+ }
+
+ public function zrangebylex(mixed $key, mixed $min, mixed $max, int $offset = -1, int $count = -1): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrangebylex(...\func_get_args());
+ }
+
+ public function zrevrangebylex(mixed $key, mixed $max, mixed $min, int $offset = -1, int $count = -1): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zrevrangebylex(...\func_get_args());
+ }
+
+ public function zrem(mixed $key, mixed ...$args): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zrem(...\func_get_args());
+ }
+
+ public function zremrangebylex(mixed $key, mixed $min, mixed $max): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zremrangebylex(...\func_get_args());
+ }
+
+ public function zremrangebyrank(mixed $key, int $start, int $end): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zremrangebyrank(...\func_get_args());
+ }
+
+ public function zremrangebyscore(mixed $key, mixed $min, mixed $max): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zremrangebyscore(...\func_get_args());
+ }
+
+ public function zcard(mixed $key): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zcard(...\func_get_args());
+ }
+
+ public function zcount(mixed $key, mixed $min, mixed $max): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zcount(...\func_get_args());
+ }
+
+ public function zdiff(array $keys, array|null $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zdiff(...\func_get_args());
+ }
+
+ public function zdiffstore(mixed $dstkey, array $keys): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zdiffstore(...\func_get_args());
+ }
+
+ public function zincrby(mixed $key, float $score, mixed $member): Cluster|false|float
+ {
+ return $this->initializeLazyObject()->zincrby(...\func_get_args());
+ }
+
+ public function zlexcount(mixed $key, mixed $min, mixed $max): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zlexcount(...\func_get_args());
+ }
+
+ public function zmscore(mixed $key, mixed ...$members): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zmscore(...\func_get_args());
+ }
+
+ public function zinter(array $keys, array|null $weights = null, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zinter(...\func_get_args());
+ }
+
+ public function zintercard(array $keys, int $limit = -1): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zintercard(...\func_get_args());
+ }
+
+ public function zinterstore(mixed $dstkey, array $keys, array|null $weights = null, mixed $options = null): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zinterstore(...\func_get_args());
+ }
+
+ public function zunion(array $keys, array|null $weights = null, mixed $options = null): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zunion(...\func_get_args());
+ }
+
+ public function zunionstore(mixed $dstkey, array $keys, array|null $weights = null, mixed $options = null): Cluster|false|int
+ {
+ return $this->initializeLazyObject()->zunionstore(...\func_get_args());
+ }
+
+ public function zpopmin(mixed $key, int $count = 1): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zpopmin(...\func_get_args());
+ }
+
+ public function zpopmax(mixed $key, int $count = 1): Cluster|array|false
+ {
+ return $this->initializeLazyObject()->zpopmax(...\func_get_args());
+ }
+
+ public function _getKeys(): array|false
+ {
+ return $this->initializeLazyObject()->_getKeys(...\func_get_args());
+ }
+
+ public function _masters(): array
+ {
+ return $this->initializeLazyObject()->_masters(...\func_get_args());
+ }
+
+ public function copy(mixed $srckey, mixed $dstkey, array|null $options = null): Cluster|bool
+ {
+ return $this->initializeLazyObject()->copy(...\func_get_args());
+ }
+}
diff --git a/composer.json b/composer.json
index bdb461be..c89d6672 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,7 @@
"php": ">=8.2",
"psr/cache": "^2.0|^3.0",
"psr/log": "^1.1|^2|^3",
- "symfony/cache-contracts": "^2.5|^3",
+ "symfony/cache-contracts": "^3.6",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/var-exporter": "^6.4|^7.0"