diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index 51118c6dfafa2..83d8a85aed96d 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -301,6 +301,7 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName, $cacheDef->addMethodCall('setMemcached', [new Reference($this->getObjectManagerElementName(\sprintf('%s_memcached_instance', $objectManagerName)))]); break; case 'redis': + case 'valkey': $redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%'; $redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.redis_instance.class').'%'; $redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.redis_host').'%'; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index ce77b50f8585f..fd77de26e6bc6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1285,6 +1285,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools/app')->end() ->scalarNode('default_psr6_provider')->end() ->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end() + ->scalarNode('default_valkey_provider')->defaultValue('valkey://localhost')->end() ->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->end() ->scalarNode('default_doctrine_dbal_provider')->defaultValue('database_connection')->end() ->scalarNode('default_pdo_provider')->defaultValue($willBeAvailable('doctrine/dbal', Connection::class) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null)->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 45f4366c17d19..418ee739c6863 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2501,7 +2501,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con // Inline any env vars referenced in the parameter $container->setParameter('cache.prefix.seed', $container->resolveEnvPlaceholders($container->getParameter('cache.prefix.seed'), true)); } - foreach (['psr6', 'redis', 'memcached', 'doctrine_dbal', 'pdo'] as $name) { + foreach (['psr6', 'redis', 'valkey', 'memcached', 'doctrine_dbal', 'pdo'] as $name) { if (isset($config[$name = 'default_'.$name.'_provider'])) { $container->setAlias('cache.'.$name, new Alias(CachePoolPass::getServiceProvider($container, $config[$name]), false)); } @@ -2513,12 +2513,13 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con 'tags' => false, ]; } + $redisTagAwareAdapters = [['cache.adapter.redis_tag_aware'], ['cache.adapter.valkey_tag_aware']]; foreach ($config['pools'] as $name => $pool) { $pool['adapters'] = $pool['adapters'] ?: ['cache.app']; - $isRedisTagAware = ['cache.adapter.redis_tag_aware'] === $pool['adapters']; + $isRedisTagAware = \in_array($pool['adapters'], $redisTagAwareAdapters, true); foreach ($pool['adapters'] as $provider => $adapter) { - if (($config['pools'][$adapter]['adapters'] ?? null) === ['cache.adapter.redis_tag_aware']) { + if (\in_array($config['pools'][$adapter]['adapters'] ?? null, $redisTagAwareAdapters, true)) { $isRedisTagAware = true; } elseif ($config['pools'][$adapter]['tags'] ?? false) { $pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner'; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php index ad4dca42d3b78..3d96ba05994ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php @@ -140,6 +140,7 @@ 'reset' => 'reset', ]) ->tag('monolog.logger', ['channel' => 'cache']) + ->alias('cache.adapter.valkey', 'cache.adapter.redis') ->set('cache.adapter.redis_tag_aware', RedisTagAwareAdapter::class) ->abstract() @@ -156,6 +157,7 @@ 'reset' => 'reset', ]) ->tag('monolog.logger', ['channel' => 'cache']) + ->alias('cache.adapter.valkey_tag_aware', 'cache.adapter.redis_tag_aware') ->set('cache.adapter.memcached', MemcachedAdapter::class) ->abstract() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 6842cf9e92705..e4ec77451724b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -883,6 +883,7 @@ protected static function getBundleDefaultConfig() 'system' => 'cache.adapter.system', 'directory' => '%kernel.cache_dir%/pools/app', 'default_redis_provider' => 'redis://localhost', + 'default_valkey_provider' => 'valkey://localhost', 'default_memcached_provider' => 'memcached://localhost', 'default_doctrine_dbal_provider' => 'database_connection', 'default_pdo_provider' => ContainerBuilder::willBeAvailable('doctrine/dbal', Connection::class, ['symfony/framework-bundle']) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null, diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index c03868da12535..8cd2218dc06c1 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -111,7 +111,7 @@ 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:')) { @@ -128,7 +128,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 diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index b92cd754c7746..59195f03edbfa 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * Add support for `\Relay\Cluster` in `RedisAdapter` + * Add support for `valkey:` / `valkeys:` schemes + * Rename options "redis_cluster" and "redis_sentinel" to "cluster" and "sentinel" respectively 7.2 --- diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php index 6dc13b8194f8d..9103eec59622d 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php +++ b/src/Symfony/Component/Cache/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/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 480e4f77ad021..09a8e23cbdec7 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -46,9 +46,10 @@ trait RedisTrait 'retry_interval' => 0, 'tcp_keepalive' => 0, 'lazy' => null, - 'redis_cluster' => false, - 'relay_cluster_context' => [], 'redis_sentinel' => null, + 'cluster' => false, + 'sentinel' => null, + 'relay_cluster_context' => [], 'dbindex' => 0, 'failover' => 'none', 'ssl' => null, // see https://php.net/context.ssl @@ -90,13 +91,13 @@ private function init(\Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predi */ 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.'); @@ -124,7 +125,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'])) { @@ -177,32 +178,41 @@ 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); } + $params['cluster'] = filter_var($params['cluster'], \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'] => match (true) { + $params['cluster'] => match (true) { \extension_loaded('redis') => \RedisCluster::class, \extension_loaded('relay') => RelayCluster::class, default => \Predis\Client::class, }, - isset($params['redis_sentinel']) => match (true) { + isset($params['sentinel']) => match (true) { \extension_loaded('redis') => \Redis::class, \extension_loaded('relay') => Relay::class, default => \Predis\Client::class, @@ -213,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)); } @@ -237,7 +247,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra $host = 'tls://'.$host; } - if (!isset($params['redis_sentinel'])) { + if (!isset($params['sentinel'])) { break; } @@ -263,15 +273,15 @@ 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 { @@ -446,11 +456,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'] += [ @@ -478,7 +491,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; @@ -486,8 +499,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)) { diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 0841fa9ab5252..59070ee8b307a 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for iterable of string in `StreamedResponse` * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming + * Add support for `valkey:` / `valkeys:` schemes for sessions 7.2 --- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php index 13af07c21e8ec..cb0b6f8a9079d 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -57,6 +57,8 @@ public static function createHandler(object|string $connection, array $options = case str_starts_with($connection, 'redis:'): case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'valkey:'): + case str_starts_with($connection, 'valkeys:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); diff --git a/src/Symfony/Component/Lock/CHANGELOG.md b/src/Symfony/Component/Lock/CHANGELOG.md index df19f0a54d7c0..1ea898d7ee96d 100644 --- a/src/Symfony/Component/Lock/CHANGELOG.md +++ b/src/Symfony/Component/Lock/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add support for `valkey:` / `valkeys:` schemes + 7.2 --- diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php index 1ce98acd0e0f1..2f99458feb889 100644 --- a/src/Symfony/Component/Lock/Store/StoreFactory.php +++ b/src/Symfony/Component/Lock/Store/StoreFactory.php @@ -62,6 +62,8 @@ public static function createStore(#[\SensitiveParameter] object|string $connect case str_starts_with($connection, 'redis:'): case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'valkey:'): + case str_starts_with($connection, 'valkeys:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { throw new InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md b/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md index 9427d38f27aa5..01009c3f8e779 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Implement the `KeepaliveReceiverInterface` to enable asynchronously notifying Redis that the job is still being processed, in order to avoid timeouts + * Add support for `valkey:` / `valkeys:` schemes 6.3 --- diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php index d3fdd37c5e5c5..7d449f71c501a 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php @@ -423,7 +423,7 @@ public function testInvalidSentinelMasterName() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('Failed to retrieve master information from master name "%s" and address "%s".', $uid, $exp)); - Connection::fromDsn(\sprintf('%s/messenger-clearlasterror', $master), ['delete_after_ack' => true, 'sentinel_master' => $uid], null); + Connection::fromDsn(\sprintf('%s/messenger-clearlasterror', $master), ['delete_after_ack' => true, 'sentinel' => $uid], null); } public function testFromDsnOnUnixSocketWithUserAndPassword() diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php index 469623a9419c3..aa87d8bebbe57 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php @@ -39,7 +39,7 @@ protected function setUp(): void try { $this->redis = $this->createRedisClient(); - $this->connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), ['sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], $this->redis); + $this->connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), ['sentinel' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], $this->redis); $this->connection->cleanup(); $this->connection->setup(); } catch (\Exception $e) { @@ -147,7 +147,7 @@ public function testConnectionSendDelayedMessagesWithSameContent() public function testConnectionBelowRedeliverTimeout() { // lower redeliver timeout and claim interval - $connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), ['sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], $this->redis); + $connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), ['sentinel' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], $this->redis); $connection->cleanup(); $connection->setup(); @@ -175,7 +175,7 @@ public function testConnectionClaimAndRedeliver() // lower redeliver timeout and claim interval $connection = Connection::fromDsn( getenv('MESSENGER_REDIS_DSN'), - ['redeliver_timeout' => 0, 'claim_interval' => 500, 'sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], + ['redeliver_timeout' => 0, 'claim_interval' => 500, 'sentinel' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null], $this->redis ); @@ -254,6 +254,7 @@ public function testSentinel(string $sentinelOptionName) public static function sentinelOptionNames(): \Generator { + yield ['sentinel']; yield ['redis_sentinel']; yield ['sentinel_master']; } @@ -263,7 +264,7 @@ public function testLazySentinel() $connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), ['lazy' => true, 'delete_after_ack' => true, - 'sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null, + 'sentinel' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null, ], $this->redis); $connection->add('1', []); @@ -335,7 +336,7 @@ public function testFromDsnWithMultipleHosts() $dsn = array_map(fn ($host) => 'redis://'.$host, $hosts); $dsn = implode(',', $dsn); - $this->assertInstanceOf(Connection::class, Connection::fromDsn($dsn, ['sentinel_master' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null])); + $this->assertInstanceOf(Connection::class, Connection::fromDsn($dsn, ['sentinel' => getenv('MESSENGER_REDIS_SENTINEL_MASTER') ?: null])); } public function testJsonError() @@ -360,7 +361,7 @@ public function testGetNonBlocking() { $redis = $this->createRedisClient(); - $connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', ['sentinel_master' => null], $redis); + $connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', ['sentinel' => null], $redis); try { $this->assertNull($connection->get()); // no message, should return null immediately @@ -378,7 +379,7 @@ public function testGetNonBlocking() public function testGetAfterReject() { $redis = $this->createRedisClient(); - $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel_master' => null], $redis); + $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel' => null], $redis); try { $connection->add('1', []); @@ -387,7 +388,7 @@ public function testGetAfterReject() $failing = $connection->get(); $connection->reject($failing['id']); - $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel_master' => null], $redis); + $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel' => null], $redis); $this->assertNotNull($connection->get()); } finally { $redis->unlink('messenger-rejectthenget'); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php index 58c7cf0d05637..2af6860d1d96e 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php @@ -28,7 +28,9 @@ public function testSupportsOnlyRedisTransports() $this->assertTrue($factory->supports('redis://localhost', [])); $this->assertTrue($factory->supports('rediss://localhost', [])); - $this->assertTrue($factory->supports('redis:?host[host1:5000]&host[host2:5000]&host[host3:5000]&sentinel_master=test&dbindex=0', [])); + $this->assertTrue($factory->supports('valkey://localhost', [])); + $this->assertTrue($factory->supports('valkeys://localhost', [])); + $this->assertTrue($factory->supports('redis:?host[host1:5000]&host[host2:5000]&host[host3:5000]&sentinel=test&dbindex=0', [])); $this->assertFalse($factory->supports('sqs://localhost', [])); $this->assertFalse($factory->supports('invalid-dsn', [])); } @@ -69,7 +71,7 @@ public static function createTransportProvider(): iterable if (false !== getenv('REDIS_SENTINEL_HOSTS') && false !== getenv('REDIS_SENTINEL_SERVICE')) { yield 'redis_sentinel' => [ 'redis:?host['.str_replace(' ', ']&host[', getenv('REDIS_SENTINEL_HOSTS')).']', - ['sentinel_master' => getenv('REDIS_SENTINEL_SERVICE')], + ['sentinel' => getenv('REDIS_SENTINEL_SERVICE')], ]; } } diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php index cc78567009121..27e7d7b2381af 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php @@ -46,8 +46,7 @@ class Connection 'lazy' => false, 'auth' => null, 'serializer' => 1, // see \Redis::SERIALIZER_PHP, - 'sentinel_master' => null, // String, master to look for (optional, default is NULL meaning Sentinel support is disabled) - 'redis_sentinel' => null, // String, alias for 'sentinel_master' + 'sentinel' => null, // String, master to look for (optional, default is NULL meaning Sentinel support is disabled) 'timeout' => 0.0, // Float, value in seconds (optional, default is 0 meaning unlimited) 'read_timeout' => 0.0, // Float, value in seconds (optional, default is 0 meaning unlimited) 'retry_interval' => 0, // Int, value in milliseconds (optional, default is 0) @@ -80,11 +79,7 @@ public function __construct(array $options, \Redis|Relay|\RedisCluster|null $red $port = $options['port']; $auth = $options['auth']; - if (isset($options['redis_sentinel']) && isset($options['sentinel_master'])) { - throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.'); - } - - $sentinelMaster = $options['sentinel_master'] ?? $options['redis_sentinel'] ?? null; + $sentinelMaster = $options['sentinel'] ?? $options['redis_sentinel'] ?? $options['sentinel_master'] ?? null; if (null !== $sentinelMaster && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) { throw new InvalidArgumentException('Redis Sentinel support requires ext-redis>=5.2, or ext-relay.'); @@ -238,7 +233,7 @@ public static function fromDsn(#[\SensitiveParameter] string $dsn, array $option if (!str_contains($dsn, ',')) { $params = self::parseDsn($dsn, $options); - if (isset($params['host']) && 'rediss' === $params['scheme']) { + if (isset($params['host']) && ('rediss' === $params['scheme'] || 'valkeys' === $params['scheme'])) { $params['host'] = 'tls://'.$params['host']; } } else { @@ -249,7 +244,7 @@ public static function fromDsn(#[\SensitiveParameter] string $dsn, array $option // Merge all the URLs, the last one overrides the previous ones $params = array_merge(...$paramss); - $tls = 'rediss' === $params['scheme']; + $tls = 'rediss' === $params['scheme'] || 'valkeys' === $params['scheme']; // Regroup all the hosts in an array interpretable by RedisCluster $params['host'] = array_map(function ($params) use ($tls) { @@ -284,7 +279,7 @@ public static function fromDsn(#[\SensitiveParameter] string $dsn, array $option parse_str($params['query'], $query); if (isset($query['host'])) { - $tls = 'rediss' === $params['scheme']; + $tls = 'rediss' === $params['scheme'] || 'valkeys' === $params['scheme']; $tcpScheme = $tls ? 'tls' : 'tcp'; if (!\is_array($hosts = $query['host'])) { @@ -325,7 +320,13 @@ public static function fromDsn(#[\SensitiveParameter] string $dsn, array $option private static function parseDsn(string $dsn, array &$options): array { $url = $dsn; - $scheme = str_starts_with($dsn, 'rediss:') ? 'rediss' : 'redis'; + $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 (preg_match('#^'.$scheme.':///([^:@])+$#', $dsn)) { $url = str_replace($scheme.':', 'file:', $dsn); diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php index 89ebf6ee119be..632862c7fb97a 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php @@ -32,6 +32,6 @@ public function createTransport(#[\SensitiveParameter] string $dsn, array $optio public function supports(#[\SensitiveParameter] string $dsn, array $options): bool { - return str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:'); + return str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:'); } } diff --git a/src/Symfony/Component/Messenger/Transport/TransportFactory.php b/src/Symfony/Component/Messenger/Transport/TransportFactory.php index 860cba41cc077..ae0db36d31127 100644 --- a/src/Symfony/Component/Messenger/Transport/TransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/TransportFactory.php @@ -45,6 +45,8 @@ public function createTransport(#[\SensitiveParameter] string $dsn, array $optio $packageSuggestion = ' Run "composer require symfony/doctrine-messenger" to install Doctrine transport.'; } elseif (str_starts_with($dsn, 'redis://') || str_starts_with($dsn, 'rediss://')) { $packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Redis transport.'; + } elseif (str_starts_with($dsn, 'valkey://') || str_starts_with($dsn, 'valkeys://')) { + $packageSuggestion = ' Run "composer require symfony/redis-messenger" to install Valkey transport.'; } elseif (str_starts_with($dsn, 'sqs://') || preg_match('#^https://sqs\.[\w\-]+\.amazonaws\.com/.+#', $dsn)) { $packageSuggestion = ' Run "composer require symfony/amazon-sqs-messenger" to install Amazon SQS transport.'; } elseif (str_starts_with($dsn, 'beanstalkd://')) { diff --git a/src/Symfony/Component/Semaphore/CHANGELOG.md b/src/Symfony/Component/Semaphore/CHANGELOG.md index c92b74aba72ae..8ae9706e21544 100644 --- a/src/Symfony/Component/Semaphore/CHANGELOG.md +++ b/src/Symfony/Component/Semaphore/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add support for `valkey:` / `valkeys:` schemes + 6.3 --- diff --git a/src/Symfony/Component/Semaphore/Store/StoreFactory.php b/src/Symfony/Component/Semaphore/Store/StoreFactory.php index 348e116891b7e..8e3d2cf06e410 100644 --- a/src/Symfony/Component/Semaphore/Store/StoreFactory.php +++ b/src/Symfony/Component/Semaphore/Store/StoreFactory.php @@ -37,6 +37,8 @@ public static function createStore(#[\SensitiveParameter] object|string $connect throw new InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection::class)); case str_starts_with($connection, 'redis:'): case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'valkey:'): + case str_starts_with($connection, 'valkeys:'): if (!class_exists(AbstractAdapter::class)) { throw new InvalidArgumentException('Unsupported Redis DSN. Try running "composer require symfony/cache".'); }