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 <p@tchwork.com> */ -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 <p@tchwork.com> */ -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 <dunglas@gmail.com> */ -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..c3a4909e 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 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 <galopintitouan@gmail.com> */ -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 <p@tchwork.com> */ -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 <p@tchwork.com> * @author Sergey Belyshkin <sbelyshkin@gmail.com> */ -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..43628e4c 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,8 +27,9 @@ * @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Nicolas Grekas <p@tchwork.com> */ -class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +class TraceableAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface { + private string $namespace = ''; private array $calls = []; public function __construct( @@ -34,10 +37,13 @@ public function __construct( ) { } + /** + * @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)); } $isHit = true; @@ -225,11 +231,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 +270,5 @@ class TraceableAdapterEvent public array|bool $result; public int $hits = 0; public int $misses = 0; + public string $namespace; } 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/Exception/BadMethodCallException.php b/Exception/BadMethodCallException.php new file mode 100644 index 00000000..d81f9d26 --- /dev/null +++ b/Exception/BadMethodCallException.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace 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 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace 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 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace 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".<<<EOPHP + { + {$return}\$this->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".<<<EOPHP + { + {$return}\$this->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 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace 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"