From 88109a278fd535ed35b5105c288e13b6b8070992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 14:36:03 +0200 Subject: [PATCH 01/13] feat(predis): support json:/csv: env processors for DSN arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PredisParametersFactory::createFromDsns() that accepts string|array and handles the nested-array produced by Symfony's json:/csv: env processors. SncRedisExtension now uses createFromDsns for RedisEnvDsn connections so that %env(json:REDIS_DSNS)% resolves correctly at runtime. Signed-off-by: Guillaume Delré --- src/DependencyInjection/SncRedisExtension.php | 2 +- src/Factory/PredisParametersFactory.php | 30 ++++++++++++++++ .../config/yaml/env_predis_json_dsn.yaml | 9 +++++ .../SncRedisExtensionEnvTest.php | 20 +++++++++-- tests/Factory/PredisParametersFactoryTest.php | 35 +++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index be4d6cdb..26c0b9ab 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -210,7 +210,7 @@ private function loadPredisConnectionParameters(string $clientAlias, array $opti $parameterId = sprintf('snc_redis.connection.%s_parameters.%s', $options['alias'], $clientAlias); $parameterDef = new Definition($parametersClass); - $parameterDef->setFactory([PredisParametersFactory::class, 'create']); + $parameterDef->setFactory([PredisParametersFactory::class, $dsn instanceof RedisEnvDsn ? 'createFromDsns' : 'create']); $parameterDef->addArgument($options); $parameterDef->addArgument($parametersClass); $parameterDef->addArgument((string) $dsn); diff --git a/src/Factory/PredisParametersFactory.php b/src/Factory/PredisParametersFactory.php index 637fcee0..247eae58 100644 --- a/src/Factory/PredisParametersFactory.php +++ b/src/Factory/PredisParametersFactory.php @@ -9,16 +9,46 @@ use Snc\RedisBundle\DependencyInjection\Configuration\RedisDsn; use function array_filter; +use function array_map; use function array_merge; use function constant; +use function count; use function defined; use function is_a; +use function is_array; +use function is_string; use function sprintf; use function str_replace; /** @internal */ class PredisParametersFactory { + /** + * @param class-string $class + * @param array $options + * @param string|list|list> $dsns + * + * @return ParametersInterface|list + */ + public static function createFromDsns(array $options, string $class, string|array $dsns): ParametersInterface|array + { + if (is_string($dsns)) { + $dsns = [$dsns]; + } + + // json:/csv: env processors can produce a single-element array wrapping the actual list + if (count($dsns) === 1 && is_array($dsns[0])) { + $dsns = $dsns[0]; + } + + $parameters = array_map( + static fn (string $d) => static::create($options, $class, $d), + $dsns, + ); + + return count($parameters) === 1 ? $parameters[0] : $parameters; + } + /** * @param class-string $class * @param array $options diff --git a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml new file mode 100644 index 00000000..64b12b00 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml @@ -0,0 +1,9 @@ +parameters: + env(REDIS_DSNS): '"redis://localhost"' + +snc_redis: + clients: + default: + type: predis + alias: default + dsn: "%env(json:REDIS_DSNS)%" diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index 8fcb3148..a40c54b4 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -26,7 +26,7 @@ public function testPredisDefaultParameterConfigLoad(): void $container = $this->getConfiguredContainer('env_predis_minimal'); $this->assertSame( - [PredisParametersFactory::class, 'create'], + [PredisParametersFactory::class, 'createFromDsns'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); } @@ -36,11 +36,27 @@ public function testPredisDefaultParameterWithSSLContextConfigLoad(): void $container = $this->getConfiguredContainer('env_predis_ssl_context'); $this->assertSame( - [PredisParametersFactory::class, 'create'], + [PredisParametersFactory::class, 'createFromDsns'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); } + public function testPredisJsonEnvDsn(): void + { + $container = $this->getConfiguredContainer('env_predis_json_dsn'); + + $this->assertSame( + [PredisParametersFactory::class, 'createFromDsns'], + $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), + ); + + $clientDefinition = $container->findDefinition('snc_redis.default'); + $this->assertSame( + 'snc_redis.connection.default_parameters.default', + (string) $clientDefinition->getArgument(0), + ); + } + public function testPredisDefaultParameterConfig(): void { $container = $this->getConfiguredContainer('env_predis_ssl_context'); diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index f9420f8c..b9f08a58 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -185,4 +185,39 @@ public function testCreateMergesSslWithTlsVersion(): void $this->assertSame(STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, $ssl['crypto_type']); $this->assertFalse($ssl['verify_peer']); } + + public function testCreateFromDsnsString(): void + { + $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, 'redis://localhost'); + $this->assertInstanceOf(Parameters::class, $parameters); + $this->assertSame('localhost', $parameters->host); + } + + public function testCreateFromDsnsSingleElementArray(): void + { + $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, ['redis://localhost']); + $this->assertInstanceOf(Parameters::class, $parameters); + $this->assertSame('localhost', $parameters->host); + } + + public function testCreateFromDsnsMultiple(): void + { + $result = PredisParametersFactory::createFromDsns([], Parameters::class, ['redis://host1', 'redis://host2']); + $this->assertIsArray($result); + $this->assertCount(2, $result); + $this->assertInstanceOf(Parameters::class, $result[0]); + $this->assertInstanceOf(Parameters::class, $result[1]); + $this->assertSame('host1', $result[0]->host); + $this->assertSame('host2', $result[1]->host); + } + + public function testCreateFromDsnsNestedArray(): void + { + $result = PredisParametersFactory::createFromDsns([], Parameters::class, [['redis://host1', 'redis://host2']]); + $this->assertIsArray($result); + $this->assertCount(2, $result); + $this->assertInstanceOf(Parameters::class, $result[0]); + $this->assertSame('host1', $result[0]->host); + $this->assertSame('host2', $result[1]->host); + } } From a6111f3d8eb818927ba861700244db7710fb9153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 16:13:34 +0200 Subject: [PATCH 02/13] test(predis): use toArray() to avoid magic property access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Psalm flags ->host on Parameters (UndefinedMagicPropertyFetch) because the concrete class exposes fields via __get() without @property annotations, and on ParametersInterface (NoInterfaceProperties) when resolving the interface @property docblock. Replace with ->toArray()['host'] which is the typed interface API. Also adds the missing assertInstanceOf for $result[1] in testCreateFromDsnsNestedArray to ensure consistent type narrowing. Signed-off-by: Guillaume Delré --- tests/Factory/PredisParametersFactoryTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index b9f08a58..ea9af896 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -190,14 +190,14 @@ public function testCreateFromDsnsString(): void { $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, 'redis://localhost'); $this->assertInstanceOf(Parameters::class, $parameters); - $this->assertSame('localhost', $parameters->host); + $this->assertSame('localhost', $parameters->toArray()['host']); } public function testCreateFromDsnsSingleElementArray(): void { $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, ['redis://localhost']); $this->assertInstanceOf(Parameters::class, $parameters); - $this->assertSame('localhost', $parameters->host); + $this->assertSame('localhost', $parameters->toArray()['host']); } public function testCreateFromDsnsMultiple(): void @@ -207,8 +207,8 @@ public function testCreateFromDsnsMultiple(): void $this->assertCount(2, $result); $this->assertInstanceOf(Parameters::class, $result[0]); $this->assertInstanceOf(Parameters::class, $result[1]); - $this->assertSame('host1', $result[0]->host); - $this->assertSame('host2', $result[1]->host); + $this->assertSame('host1', $result[0]->toArray()['host']); + $this->assertSame('host2', $result[1]->toArray()['host']); } public function testCreateFromDsnsNestedArray(): void @@ -217,7 +217,8 @@ public function testCreateFromDsnsNestedArray(): void $this->assertIsArray($result); $this->assertCount(2, $result); $this->assertInstanceOf(Parameters::class, $result[0]); - $this->assertSame('host1', $result[0]->host); - $this->assertSame('host2', $result[1]->host); + $this->assertInstanceOf(Parameters::class, $result[1]); + $this->assertSame('host1', $result[0]->toArray()['host']); + $this->assertSame('host2', $result[1]->toArray()['host']); } } From 5d4bf1d32a8fb31e49cfad3e7fa5ab3b60295a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 21:43:40 +0200 Subject: [PATCH 03/13] fix(predis): prevent nested-array when env DSN resolves to multiple params at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a single RedisEnvDsn was configured with cluster or replication options, loadPredisClient wrapped the parameter Reference in an array ([Reference]). At runtime, if the env processor returned N parameters (e.g. json: with ["host1","host2"]), Predis received [[p1,p2]] instead of [p1,p2] and fell back to connecting to 127.0.0.1. Fix: for a single RedisEnvDsn, always pass the Reference directly. Signed-off-by: Guillaume Delré --- src/DependencyInjection/SncRedisExtension.php | 6 +++++- .../yaml/env_predis_json_dsn_with_cluster.yaml | 11 +++++++++++ .../SncRedisExtensionEnvTest.php | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index 26c0b9ab..9c4ce18a 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -185,7 +185,11 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v $container->setDefinition($optionId, $optionDef); $clientDef = new Definition($client['class'] ?? (string) $container->getParameter('snc_redis.client.class')); $clientDef->addTag('snc_redis.client', ['alias' => $client['alias']]); - if ($connectionCount === 1 && !isset($client['options']['cluster']) && !isset($client['options']['replication'])) { + // When a single env-var DSN uses json:/csv: processors it may resolve to N parameters at runtime; + // pass the Reference directly so Predis receives [p1,p2,...] instead of [[p1,p2,...]]. + $singleEnvDsn = $connectionCount === 1 && $client['dsns'][0] instanceof RedisEnvDsn; + + if ($connectionCount === 1 && (!isset($client['options']['cluster']) && !isset($client['options']['replication'])) || $singleEnvDsn) { $clientDef->addArgument(new Reference(sprintf('snc_redis.connection.%s_parameters.%s', $connectionAliases[0], $client['alias']))); } else { $connections = []; diff --git a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml new file mode 100644 index 00000000..c2c68f8d --- /dev/null +++ b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml @@ -0,0 +1,11 @@ +parameters: + env(REDIS_DSNS): '["redis://localhost1","redis://localhost2"]' + +snc_redis: + clients: + default: + type: predis + alias: default + dsn: "%env(json:REDIS_DSNS)%" + options: + cluster: "predis" diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index a40c54b4..de4ccb1d 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -57,6 +57,24 @@ public function testPredisJsonEnvDsn(): void ); } + public function testPredisJsonEnvDsnWithCluster(): void + { + $container = $this->getConfiguredContainer('env_predis_json_dsn_with_cluster'); + + $this->assertSame( + [PredisParametersFactory::class, 'createFromDsns'], + $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), + ); + + $clientDefinition = $container->findDefinition('snc_redis.default'); + + // Single Reference (not [Reference]) prevents nested-array [[p1,p2]] when env resolves to multiple DSNs. + $this->assertSame( + 'snc_redis.connection.default_parameters.default', + (string) $clientDefinition->getArgument(0), + ); + } + public function testPredisDefaultParameterConfig(): void { $container = $this->getConfiguredContainer('env_predis_ssl_context'); From dc7a1ad504fc775594872edebddec6821eaf6b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 12 May 2026 07:59:18 +0200 Subject: [PATCH 04/13] test(predis): verify json: env DSN resolves correctly for Predis cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the functional test app to exercise the env-based DSN path: replace the hardcoded cluster DSN list with %env(json:REDIS_DSNS)% and wire the controller to the cluster client. cluster: predis is required in the options so Predis knows how to handle the array of ParametersInterface produced at runtime when the env var holds multiple DSNs. Signed-off-by: Guillaume Delré --- tests/Functional/App/Controller/Controller.php | 13 +++++++++---- tests/Functional/App/config.yaml | 6 ++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index d0193daa..55ca6da9 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -13,18 +13,23 @@ namespace Snc\RedisBundle\Tests\Functional\App\Controller; -use Redis; +use Predis\ClientInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; class Controller extends AbstractController { - public function __invoke(Redis $redis): JsonResponse + public function __construct(#[Autowire(service: 'snc_redis.cluster')] private ClientInterface $cluster) { - $redis->set('foo', 'bar'); + } + + public function __invoke(): JsonResponse + { + $this->cluster->set('foo', 'bar'); return new JsonResponse([ - 'result' => $redis->get('foo'), + 'result' => $this->cluster->get('foo'), ]); } } diff --git a/tests/Functional/App/config.yaml b/tests/Functional/App/config.yaml index 64e08d7a..f3b42e5f 100644 --- a/tests/Functional/App/config.yaml +++ b/tests/Functional/App/config.yaml @@ -35,11 +35,9 @@ snc_redis: cluster: type: predis alias: cluster - dsn: - - redis://sncredis@127.0.0.1/3 - - redis://sncredis@127.0.0.1/4 - - redis://sncredis@127.0.0.1/5 + dsn: "%env(json:REDIS_DSNS)%" options: + cluster: predis prefix: foo connection_timeout: 10 connection_persistent: true From 2745cceb3852c31b0701a66da8e0736afaa61728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 12 May 2026 09:41:41 +0200 Subject: [PATCH 05/13] fix(ci): resolve coding-standards, psalm and phpunit check failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Doctrine coding standard requires PHP attributes on their own line; move #[Autowire] above the constructor parameter in Controller. Psalm flags the constructor as possibly-unused because it cannot see the Symfony DI container call, so add a targeted suppression in psalm.xml.dist. The functional test was missing the REDIS_DSNS environment variable that %env(json:REDIS_DSNS)% requires at runtime; define it in phpunit.xml.dist with the three cluster DSNs. Signed-off-by: Guillaume Delré --- phpunit.xml.dist | 1 + psalm.xml.dist | 5 +++++ tests/Functional/App/Controller/Controller.php | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d13aefab..fcb84265 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,6 +7,7 @@ + diff --git a/psalm.xml.dist b/psalm.xml.dist index c7f96af7..6b7ae216 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -19,6 +19,11 @@ + + + + + diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index 55ca6da9..3de5e408 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -20,8 +20,10 @@ class Controller extends AbstractController { - public function __construct(#[Autowire(service: 'snc_redis.cluster')] private ClientInterface $cluster) - { + public function __construct( + #[Autowire(service: 'snc_redis.cluster')] + private ClientInterface $cluster, + ) { } public function __invoke(): JsonResponse From e3878c5a2bbb07f4255f4b1a0dfa87c1937c6cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 08:36:53 +0200 Subject: [PATCH 06/13] refactor(predis): merge createFromDsns into create and fix condition logic Fold `createFromDsns` into `create` (now accepts `string|array $dsn`) to keep the public surface minimal; internal per-DSN work moves to private `createFromSingleDsn`. Replace the precedence-sensitive `$count === 1 || $singleEnvDsn` condition in `loadPredisClient` with an explicit `$hasAggregation` flag. Update tests accordingly. --- src/DependencyInjection/SncRedisExtension.php | 10 ++++---- src/Factory/PredisParametersFactory.php | 18 +++++++-------- .../SncRedisExtensionEnvTest.php | 18 ++++++++------- tests/Factory/PredisParametersFactoryTest.php | 23 +++++-------------- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index 9c4ce18a..9d452657 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -185,11 +185,11 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v $container->setDefinition($optionId, $optionDef); $clientDef = new Definition($client['class'] ?? (string) $container->getParameter('snc_redis.client.class')); $clientDef->addTag('snc_redis.client', ['alias' => $client['alias']]); - // When a single env-var DSN uses json:/csv: processors it may resolve to N parameters at runtime; - // pass the Reference directly so Predis receives [p1,p2,...] instead of [[p1,p2,...]]. - $singleEnvDsn = $connectionCount === 1 && $client['dsns'][0] instanceof RedisEnvDsn; - if ($connectionCount === 1 && (!isset($client['options']['cluster']) && !isset($client['options']['replication'])) || $singleEnvDsn) { + $singleEnvDsn = $connectionCount === 1 && $client['dsns'][0] instanceof RedisEnvDsn; + $hasAggregation = !$singleEnvDsn && (isset($client['options']['cluster']) || isset($client['options']['replication'])); + + if ($connectionCount === 1 && !$hasAggregation) { $clientDef->addArgument(new Reference(sprintf('snc_redis.connection.%s_parameters.%s', $connectionAliases[0], $client['alias']))); } else { $connections = []; @@ -214,7 +214,7 @@ private function loadPredisConnectionParameters(string $clientAlias, array $opti $parameterId = sprintf('snc_redis.connection.%s_parameters.%s', $options['alias'], $clientAlias); $parameterDef = new Definition($parametersClass); - $parameterDef->setFactory([PredisParametersFactory::class, $dsn instanceof RedisEnvDsn ? 'createFromDsns' : 'create']); + $parameterDef->setFactory([PredisParametersFactory::class, 'create']); $parameterDef->addArgument($options); $parameterDef->addArgument($parametersClass); $parameterDef->addArgument((string) $dsn); diff --git a/src/Factory/PredisParametersFactory.php b/src/Factory/PredisParametersFactory.php index 247eae58..7e10614a 100644 --- a/src/Factory/PredisParametersFactory.php +++ b/src/Factory/PredisParametersFactory.php @@ -26,24 +26,24 @@ class PredisParametersFactory /** * @param class-string $class * @param array $options - * @param string|list|list> $dsns + * @param string|list|list> $dsn * * @return ParametersInterface|list */ - public static function createFromDsns(array $options, string $class, string|array $dsns): ParametersInterface|array + public static function create(array $options, string $class, string|array $dsn): ParametersInterface|array { - if (is_string($dsns)) { - $dsns = [$dsns]; + if (is_string($dsn)) { + $dsn = [$dsn]; } // json:/csv: env processors can produce a single-element array wrapping the actual list - if (count($dsns) === 1 && is_array($dsns[0])) { - $dsns = $dsns[0]; + if (count($dsn) === 1 && is_array($dsn[0])) { + $dsn = $dsn[0]; } $parameters = array_map( - static fn (string $d) => static::create($options, $class, $d), - $dsns, + static fn (string $d) => static::createFromSingleDsn($options, $class, $d), + $dsn, ); return count($parameters) === 1 ? $parameters[0] : $parameters; @@ -53,7 +53,7 @@ public static function createFromDsns(array $options, string $class, string|arra * @param class-string $class * @param array $options */ - public static function create(array $options, string $class, string $dsn): ParametersInterface + private static function createFromSingleDsn(array $options, string $class, string $dsn): ParametersInterface { if (!is_a($class, ParametersInterface::class, true)) { throw new InvalidArgumentException(sprintf('%s::%s requires $class argument to implement %s', self::class, __METHOD__, ParametersInterface::class)); diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index de4ccb1d..dad47c56 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\Reference; use function array_key_exists; use function sys_get_temp_dir; @@ -26,7 +27,7 @@ public function testPredisDefaultParameterConfigLoad(): void $container = $this->getConfiguredContainer('env_predis_minimal'); $this->assertSame( - [PredisParametersFactory::class, 'createFromDsns'], + [PredisParametersFactory::class, 'create'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); } @@ -36,7 +37,7 @@ public function testPredisDefaultParameterWithSSLContextConfigLoad(): void $container = $this->getConfiguredContainer('env_predis_ssl_context'); $this->assertSame( - [PredisParametersFactory::class, 'createFromDsns'], + [PredisParametersFactory::class, 'create'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); } @@ -46,7 +47,7 @@ public function testPredisJsonEnvDsn(): void $container = $this->getConfiguredContainer('env_predis_json_dsn'); $this->assertSame( - [PredisParametersFactory::class, 'createFromDsns'], + [PredisParametersFactory::class, 'create'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); @@ -62,16 +63,17 @@ public function testPredisJsonEnvDsnWithCluster(): void $container = $this->getConfiguredContainer('env_predis_json_dsn_with_cluster'); $this->assertSame( - [PredisParametersFactory::class, 'createFromDsns'], + [PredisParametersFactory::class, 'create'], $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), ); $clientDefinition = $container->findDefinition('snc_redis.default'); - // Single Reference (not [Reference]) prevents nested-array [[p1,p2]] when env resolves to multiple DSNs. - $this->assertSame( - 'snc_redis.connection.default_parameters.default', - (string) $clientDefinition->getArgument(0), + // The client receives a single Reference even with cluster enabled, so that + // PredisParametersFactory::create() can return a list of parameters at runtime. + $this->assertInstanceOf( + Reference::class, + $clientDefinition->getArgument(0), ); } diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index ea9af896..e5de98ff 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -186,34 +186,23 @@ public function testCreateMergesSslWithTlsVersion(): void $this->assertFalse($ssl['verify_peer']); } - public function testCreateFromDsnsString(): void + public function testCreateWithString(): void { - $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, 'redis://localhost'); + $parameters = PredisParametersFactory::create([], Parameters::class, 'redis://localhost'); $this->assertInstanceOf(Parameters::class, $parameters); $this->assertSame('localhost', $parameters->toArray()['host']); } - public function testCreateFromDsnsSingleElementArray(): void + public function testCreateWithSingleElementArray(): void { - $parameters = PredisParametersFactory::createFromDsns([], Parameters::class, ['redis://localhost']); + $parameters = PredisParametersFactory::create([], Parameters::class, ['redis://localhost']); $this->assertInstanceOf(Parameters::class, $parameters); $this->assertSame('localhost', $parameters->toArray()['host']); } - public function testCreateFromDsnsMultiple(): void + public function testCreateWithMultipleDsns(): void { - $result = PredisParametersFactory::createFromDsns([], Parameters::class, ['redis://host1', 'redis://host2']); - $this->assertIsArray($result); - $this->assertCount(2, $result); - $this->assertInstanceOf(Parameters::class, $result[0]); - $this->assertInstanceOf(Parameters::class, $result[1]); - $this->assertSame('host1', $result[0]->toArray()['host']); - $this->assertSame('host2', $result[1]->toArray()['host']); - } - - public function testCreateFromDsnsNestedArray(): void - { - $result = PredisParametersFactory::createFromDsns([], Parameters::class, [['redis://host1', 'redis://host2']]); + $result = PredisParametersFactory::create([], Parameters::class, ['redis://host1', 'redis://host2']); $this->assertIsArray($result); $this->assertCount(2, $result); $this->assertInstanceOf(Parameters::class, $result[0]); From 26f0cec41e6c9d7913554ef170a080644f680251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 12:06:42 +0200 Subject: [PATCH 07/13] refactor(predis): address review feedback on env DSN support - Fix flock race condition in gen-tls-certs.sh when two processes generate TLS certs simultaneously under foreman - Simplify connectionCount condition in SncRedisExtension: use single Reference for non-replication connections, array for sentinel/multi-DSN - Replace separate factory test methods with @testWith annotations - Remove confusing SncRedisExtensionEnvTest methods and obsolete fixtures - Switch functional test controller to TaggedIterator over all clients - Fix with_acl Predis client auth via DSN credentials on port 7099 - Update Redis command count assertion from 5 to 13 --- .../workflows/redis-configs/gen-tls-certs.sh | 2 +- psalm.xml.dist | 5 --- src/DependencyInjection/SncRedisExtension.php | 5 +-- .../config/yaml/env_predis_json_dsn.yaml | 9 ----- .../env_predis_json_dsn_with_cluster.yaml | 11 ------ .../SncRedisExtensionEnvTest.php | 37 ------------------- tests/Factory/PredisParametersFactoryTest.php | 31 +++++++--------- .../Functional/App/Controller/Controller.php | 20 ++++++---- tests/Functional/App/config.yaml | 6 +-- tests/Functional/IntegrationTest.php | 4 +- 10 files changed, 31 insertions(+), 99 deletions(-) delete mode 100644 tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml delete mode 100644 tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml diff --git a/.github/workflows/redis-configs/gen-tls-certs.sh b/.github/workflows/redis-configs/gen-tls-certs.sh index 69cdabf3..70066e8e 100755 --- a/.github/workflows/redis-configs/gen-tls-certs.sh +++ b/.github/workflows/redis-configs/gen-tls-certs.sh @@ -12,4 +12,4 @@ mkdir -p /tmp/redis-tls 2>/dev/null cp /tmp/redis-tls/server.crt /tmp/redis-tls/ca.crt fi -) 9>/tmp/redis-tls.lock \ No newline at end of file +) 9>/tmp/redis-tls.lock diff --git a/psalm.xml.dist b/psalm.xml.dist index 6b7ae216..c7f96af7 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -19,11 +19,6 @@ - - - - - diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index 9d452657..d348e9fe 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -186,10 +186,7 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v $clientDef = new Definition($client['class'] ?? (string) $container->getParameter('snc_redis.client.class')); $clientDef->addTag('snc_redis.client', ['alias' => $client['alias']]); - $singleEnvDsn = $connectionCount === 1 && $client['dsns'][0] instanceof RedisEnvDsn; - $hasAggregation = !$singleEnvDsn && (isset($client['options']['cluster']) || isset($client['options']['replication'])); - - if ($connectionCount === 1 && !$hasAggregation) { + if ($connectionCount === 1 && !isset($client['options']['replication'])) { $clientDef->addArgument(new Reference(sprintf('snc_redis.connection.%s_parameters.%s', $connectionAliases[0], $client['alias']))); } else { $connections = []; diff --git a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml deleted file mode 100644 index 64b12b00..00000000 --- a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn.yaml +++ /dev/null @@ -1,9 +0,0 @@ -parameters: - env(REDIS_DSNS): '"redis://localhost"' - -snc_redis: - clients: - default: - type: predis - alias: default - dsn: "%env(json:REDIS_DSNS)%" diff --git a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml b/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml deleted file mode 100644 index c2c68f8d..00000000 --- a/tests/DependencyInjection/Fixtures/config/yaml/env_predis_json_dsn_with_cluster.yaml +++ /dev/null @@ -1,11 +0,0 @@ -parameters: - env(REDIS_DSNS): '["redis://localhost1","redis://localhost2"]' - -snc_redis: - clients: - default: - type: predis - alias: default - dsn: "%env(json:REDIS_DSNS)%" - options: - cluster: "predis" diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index dad47c56..8f44354b 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -15,8 +15,6 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -use Symfony\Component\DependencyInjection\Reference; - use function array_key_exists; use function sys_get_temp_dir; @@ -42,41 +40,6 @@ public function testPredisDefaultParameterWithSSLContextConfigLoad(): void ); } - public function testPredisJsonEnvDsn(): void - { - $container = $this->getConfiguredContainer('env_predis_json_dsn'); - - $this->assertSame( - [PredisParametersFactory::class, 'create'], - $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), - ); - - $clientDefinition = $container->findDefinition('snc_redis.default'); - $this->assertSame( - 'snc_redis.connection.default_parameters.default', - (string) $clientDefinition->getArgument(0), - ); - } - - public function testPredisJsonEnvDsnWithCluster(): void - { - $container = $this->getConfiguredContainer('env_predis_json_dsn_with_cluster'); - - $this->assertSame( - [PredisParametersFactory::class, 'create'], - $container->findDefinition('snc_redis.connection.default_parameters.default')->getFactory(), - ); - - $clientDefinition = $container->findDefinition('snc_redis.default'); - - // The client receives a single Reference even with cluster enabled, so that - // PredisParametersFactory::create() can return a list of parameters at runtime. - $this->assertInstanceOf( - Reference::class, - $clientDefinition->getArgument(0), - ); - } - public function testPredisDefaultParameterConfig(): void { $container = $this->getConfiguredContainer('env_predis_ssl_context'); diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index e5de98ff..a504395d 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -186,28 +186,25 @@ public function testCreateMergesSslWithTlsVersion(): void $this->assertFalse($ssl['verify_peer']); } - public function testCreateWithString(): void - { - $parameters = PredisParametersFactory::create([], Parameters::class, 'redis://localhost'); - $this->assertInstanceOf(Parameters::class, $parameters); - $this->assertSame('localhost', $parameters->toArray()['host']); - } - - public function testCreateWithSingleElementArray(): void + /** + * @testWith ["redis://localhost"] + * [["redis://localhost"]] + * [[["redis://localhost"]]] + */ + public function testCreateReturnsSingleParameters(string|array $dsn): void { - $parameters = PredisParametersFactory::create([], Parameters::class, ['redis://localhost']); - $this->assertInstanceOf(Parameters::class, $parameters); - $this->assertSame('localhost', $parameters->toArray()['host']); + $result = PredisParametersFactory::create([], Parameters::class, $dsn); + $this->assertInstanceOf(Parameters::class, $result); } - public function testCreateWithMultipleDsns(): void + /** + * @testWith [["redis://host1", "redis://host2"]] + * [[["redis://host1", "redis://host2"]]] + */ + public function testCreateReturnsMultipleParameters(array $dsn): void { - $result = PredisParametersFactory::create([], Parameters::class, ['redis://host1', 'redis://host2']); + $result = PredisParametersFactory::create([], Parameters::class, $dsn); $this->assertIsArray($result); $this->assertCount(2, $result); - $this->assertInstanceOf(Parameters::class, $result[0]); - $this->assertInstanceOf(Parameters::class, $result[1]); - $this->assertSame('host1', $result[0]->toArray()['host']); - $this->assertSame('host2', $result[1]->toArray()['host']); } } diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index 3de5e408..d69b4b9f 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -13,25 +13,29 @@ namespace Snc\RedisBundle\Tests\Functional\App\Controller; -use Predis\ClientInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use Symfony\Component\HttpFoundation\JsonResponse; class Controller extends AbstractController { + /** @param iterable $clients */ public function __construct( - #[Autowire(service: 'snc_redis.cluster')] - private ClientInterface $cluster, + #[TaggedIterator('snc_redis.client')] + private iterable $clients, ) { } public function __invoke(): JsonResponse { - $this->cluster->set('foo', 'bar'); + $result = null; + foreach ($this->clients as $client) { + /** @psalm-suppress MixedMethodCall */ + $client->set('foo', 'bar'); + /** @psalm-suppress MixedMethodCall */ + $result = $client->get('foo'); + } - return new JsonResponse([ - 'result' => $this->cluster->get('foo'), - ]); + return new JsonResponse(['result' => $result]); } } diff --git a/tests/Functional/App/config.yaml b/tests/Functional/App/config.yaml index f3b42e5f..150d21b0 100644 --- a/tests/Functional/App/config.yaml +++ b/tests/Functional/App/config.yaml @@ -57,12 +57,8 @@ snc_redis: with_acl: type: predis alias: with_acl - dsn: redis://localhost/1 + dsn: redis://snc_redis:snc_password@localhost:7099/1 logging: false - options: - parameters: - username: my_user - password: sncredis services: _defaults: diff --git a/tests/Functional/IntegrationTest.php b/tests/Functional/IntegrationTest.php index a806c8d3..06080b55 100644 --- a/tests/Functional/IntegrationTest.php +++ b/tests/Functional/IntegrationTest.php @@ -69,8 +69,8 @@ public function testIntegration(): void $this->assertInstanceOf(RedisDataCollector::class, $collector); $this->assertInstanceOf(ResetInterface::class, $container = $this->client->getKernel()->getContainer()); $this->assertInstanceOf(RedisLogger::class, $redisLogger = $container->get('test.snc_redis.logger')); - $this->assertSame(5, $redisLogger->getNbCommands()); - $this->assertCount(5, $collector->getCommands()); + $this->assertSame(13, $redisLogger->getNbCommands()); + $this->assertCount(13, $collector->getCommands()); $container->reset(); $this->assertSame(0, $redisLogger->getNbCommands()); } From a7c62fe5c638ee67cd6677f05561afecd3f464d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 13:02:36 +0200 Subject: [PATCH 08/13] fix(ci): fix Symfony 8.x and coding standards failures - Replace #[TaggedIterator] with explicit !tagged_iterator in config.yaml (attribute removed in Symfony 8.x) - Move Predis\Client $predisReplication binding from _defaults to PredisReplication service (Symfony 8.x strict binding validation) - Add @param annotation to testCreateReturnsMultipleParameters (phpcs) - Add blank line between use groups in SncRedisExtensionEnvTest (phpcs) - Add PossiblyUnusedMethod suppression for Controller::__construct (psalm) --- psalm.xml.dist | 5 +++++ tests/DependencyInjection/SncRedisExtensionEnvTest.php | 1 + tests/Factory/PredisParametersFactoryTest.php | 2 ++ tests/Functional/App/Controller/Controller.php | 7 ++----- tests/Functional/App/config.yaml | 10 ++++++++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index c7f96af7..6b7ae216 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -19,6 +19,11 @@ + + + + + diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index 8f44354b..8fcb3148 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + use function array_key_exists; use function sys_get_temp_dir; diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index a504395d..830d7053 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -200,6 +200,8 @@ public function testCreateReturnsSingleParameters(string|array $dsn): void /** * @testWith [["redis://host1", "redis://host2"]] * [[["redis://host1", "redis://host2"]]] + * + * @param array> $dsn */ public function testCreateReturnsMultipleParameters(array $dsn): void { diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index d69b4b9f..4eb2b9a1 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -14,16 +14,13 @@ namespace Snc\RedisBundle\Tests\Functional\App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use Symfony\Component\HttpFoundation\JsonResponse; class Controller extends AbstractController { /** @param iterable $clients */ - public function __construct( - #[TaggedIterator('snc_redis.client')] - private iterable $clients, - ) { + public function __construct(private iterable $clients) + { } public function __invoke(): JsonResponse diff --git a/tests/Functional/App/config.yaml b/tests/Functional/App/config.yaml index 150d21b0..38a3bfc4 100644 --- a/tests/Functional/App/config.yaml +++ b/tests/Functional/App/config.yaml @@ -65,8 +65,6 @@ services: autowire: true autoconfigure: true public: false - bind: - Predis\Client $predisReplication: '@snc_redis.predis_replication' Redis: '@snc_redis.default' @@ -81,6 +79,14 @@ services: resource: './Controller' tags: ['controller.service_arguments'] + Snc\RedisBundle\Tests\Functional\App\Controller\PredisReplication: + bind: + Predis\Client $predisReplication: '@snc_redis.predis_replication' + + Snc\RedisBundle\Tests\Functional\App\Controller\Controller: + arguments: + $clients: !tagged_iterator snc_redis.client + var_dumper.cli_dumper: class: Symfony\Component\VarDumper\Dumper\CliDumper arguments: ['/dev/null'] \ No newline at end of file From 53240b2d8108a4dc70bbe0f6bf136a2eb17e08b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 13:07:36 +0200 Subject: [PATCH 09/13] style: fix annotation group order in testCreateReturnsMultipleParameters --- tests/Factory/PredisParametersFactoryTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Factory/PredisParametersFactoryTest.php b/tests/Factory/PredisParametersFactoryTest.php index 830d7053..c354cea0 100644 --- a/tests/Factory/PredisParametersFactoryTest.php +++ b/tests/Factory/PredisParametersFactoryTest.php @@ -198,10 +198,10 @@ public function testCreateReturnsSingleParameters(string|array $dsn): void } /** + * @param array> $dsn + * * @testWith [["redis://host1", "redis://host2"]] * [[["redis://host1", "redis://host2"]]] - * - * @param array> $dsn */ public function testCreateReturnsMultipleParameters(array $dsn): void { From f1cc8cbbef823e1f7995b5f30a03194520f7e55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 14:17:54 +0200 Subject: [PATCH 10/13] refactor(predis): fix Controller psalm types and revert gen-tls-certs trailing newline - Type $clients as iterable to remove @psalm-suppress MixedMethodCall - Remove trailing newline from gen-tls-certs.sh to match upstream --- .github/workflows/redis-configs/gen-tls-certs.sh | 2 +- tests/Functional/App/Controller/Controller.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/redis-configs/gen-tls-certs.sh b/.github/workflows/redis-configs/gen-tls-certs.sh index 70066e8e..69cdabf3 100755 --- a/.github/workflows/redis-configs/gen-tls-certs.sh +++ b/.github/workflows/redis-configs/gen-tls-certs.sh @@ -12,4 +12,4 @@ mkdir -p /tmp/redis-tls 2>/dev/null cp /tmp/redis-tls/server.crt /tmp/redis-tls/ca.crt fi -) 9>/tmp/redis-tls.lock +) 9>/tmp/redis-tls.lock \ No newline at end of file diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index 4eb2b9a1..1b84da9e 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -13,12 +13,14 @@ namespace Snc\RedisBundle\Tests\Functional\App\Controller; +use Predis\ClientInterface; +use Redis; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; class Controller extends AbstractController { - /** @param iterable $clients */ + /** @param iterable $clients */ public function __construct(private iterable $clients) { } @@ -27,9 +29,7 @@ public function __invoke(): JsonResponse { $result = null; foreach ($this->clients as $client) { - /** @psalm-suppress MixedMethodCall */ $client->set('foo', 'bar'); - /** @psalm-suppress MixedMethodCall */ $result = $client->get('foo'); } From a89e66775f7a5e855d49c859173413e5ae78a80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 16:10:38 +0200 Subject: [PATCH 11/13] refactor(psalm): add psalm/plugin-symfony with compiled container - Add psalm/plugin-symfony ^5.0 and bump vimeo/psalm to ^6 || ^7 - Boot the test kernel in SA workflow to produce the compiled container XML - Configure the plugin with the container XML path so it resolves service constructor types and suppresses PossiblyUnusedMethod on Controller::__construct without an explicit suppression - Remove now-redundant (string)/(bool) casts on getParameter() calls whose return types are now inferred from the container XML - Drop the UnusedMethodCall suppression for NodeBuilder::end, handled natively by the plugin --- .github/workflows/static-analysis.yml | 9 ++++++++- composer.json | 3 ++- psalm.xml.dist | 15 +++------------ src/DependencyInjection/Compiler/LoggingPass.php | 2 +- src/DependencyInjection/SncRedisExtension.php | 10 +++++----- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 752d7180..f4057634 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -18,11 +18,18 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - extensions: "relay" + extensions: "redis, relay" php-version: "8.4" - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v2" + - name: "Compile Symfony container" + run: | + php -r " + require 'vendor/autoload.php'; + (new Snc\RedisBundle\Tests\Functional\App\Kernel('test', true))->boot(); + " + - name: "Run a static analysis with vimeo/psalm" run: "vendor/bin/psalm --show-info=false --stats --output-format=github --threads=$(nproc)" diff --git a/composer.json b/composer.json index 4c4d41c0..2f3e74d3 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "phpunit/phpunit": "^9.6.34 || ^10.5.63", "predis/predis": "^3.1", "psalm/plugin-phpunit": "^0.19.3", + "psalm/plugin-symfony": "^5.0", "psr/http-message": "^2", "seec/phpunit-consecutive-params": "^1.1.4", "symfony/browser-kit": "^6.4 || ^7.3 || ^8.0", @@ -49,7 +50,7 @@ "symfony/twig-bundle": "^6.4 || ^7.3 || ^8.0", "symfony/web-profiler-bundle": "^6.4 || ^7.3 || ^8.0", "symfony/yaml": "^6.4 || ^7.3 || ^8.0", - "vimeo/psalm": "^5 || ^6 || ^7" + "vimeo/psalm": "^6 || ^7" }, "conflict": { "ext-redis": "<6.1.0", diff --git a/psalm.xml.dist b/psalm.xml.dist index 6b7ae216..77e6d876 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -13,19 +13,10 @@ - - - - - - - - - - - - + + tests/Functional/App/var/cache/test/Snc_RedisBundle_Tests_Functional_App_KernelTestDebugContainer.xml + diff --git a/src/DependencyInjection/Compiler/LoggingPass.php b/src/DependencyInjection/Compiler/LoggingPass.php index 6b8cf7b7..65711ef1 100644 --- a/src/DependencyInjection/Compiler/LoggingPass.php +++ b/src/DependencyInjection/Compiler/LoggingPass.php @@ -45,7 +45,7 @@ public function process(ContainerBuilder $container): void $arguments = $option->getArgument(0); $connectionFactoryId = sprintf('snc_redis.%s_connectionfactory', $clientAlias); - $connectionFactoryDef = new Definition((string) $container->getParameter('snc_redis.connection_factory.class')); + $connectionFactoryDef = new Definition($container->getParameter('snc_redis.connection_factory.class')); if ($container->getParameter('kernel.debug')) { $connectionFactoryDef->addMethodCall('setStopwatch', [new Reference('debug.stopwatch', ContainerInterface::NULL_ON_INVALID_REFERENCE)]); } diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index d348e9fe..b7ec4d66 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -180,10 +180,10 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v } $optionId = sprintf('snc_redis.client.%s_options', $client['alias']); - $optionDef = new Definition((string) $container->getParameter('snc_redis.client_options.class')); + $optionDef = new Definition($container->getParameter('snc_redis.client_options.class')); $optionDef->addArgument($client['options']); $container->setDefinition($optionId, $optionDef); - $clientDef = new Definition($client['class'] ?? (string) $container->getParameter('snc_redis.client.class')); + $clientDef = new Definition($client['class'] ?? $container->getParameter('snc_redis.client.class')); $clientDef->addTag('snc_redis.client', ['alias' => $client['alias']]); if ($connectionCount === 1 && !isset($client['options']['replication'])) { @@ -207,7 +207,7 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v */ private function loadPredisConnectionParameters(string $clientAlias, array $options, ContainerBuilder $container, object $dsn): void { - $parametersClass = (string) $container->getParameter('snc_redis.connection_parameters.class'); + $parametersClass = $container->getParameter('snc_redis.connection_parameters.class'); $parameterId = sprintf('snc_redis.connection.%s_parameters.%s', $options['alias'], $clientAlias); $parameterDef = new Definition($parametersClass); @@ -262,7 +262,7 @@ private function loadMonolog(array $config, ContainerBuilder $container): void { $ref = new Reference(sprintf('snc_redis.%s', $config['monolog']['client'])); - $def = new Definition((string) $container->getParameter('snc_redis.monolog_handler.class'), [ + $def = new Definition($container->getParameter('snc_redis.monolog_handler.class'), [ $ref, $config['monolog']['key'], ]); @@ -278,6 +278,6 @@ private function loadMonolog(array $config, ContainerBuilder $container): void #[Override] public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface { - return new Configuration((bool) $container->getParameter('kernel.debug')); + return new Configuration($container->getParameter('kernel.debug')); } } From 873abd05343651c3ad7327efe888c11bf19c357c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 16:13:40 +0200 Subject: [PATCH 12/13] style(tests): expand Controller $clients param type to include Relay and RedisArray --- tests/Functional/App/Controller/Controller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Functional/App/Controller/Controller.php b/tests/Functional/App/Controller/Controller.php index 1b84da9e..3136e9cb 100644 --- a/tests/Functional/App/Controller/Controller.php +++ b/tests/Functional/App/Controller/Controller.php @@ -15,12 +15,14 @@ use Predis\ClientInterface; use Redis; +use RedisArray; +use Relay\Relay; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; class Controller extends AbstractController { - /** @param iterable $clients */ + /** @param iterable $clients */ public function __construct(private iterable $clients) { } From f0bd24a4d7ee553ff5845f9983c7d499886613a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Tue, 19 May 2026 16:30:41 +0200 Subject: [PATCH 13/13] fix(psalm): suppress NodeBuilder/NodeParentInterface end() for Symfony 7+ compat Add suppressions for UnusedMethodCall (NodeBuilder::end) and UndefinedInterfaceMethod (NodeParentInterface::end), which surface when running against Symfony 7+. Set findUnusedIssueHandlerSuppression=false to avoid false positives on environments where these are not triggered. --- psalm.xml.dist | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/psalm.xml.dist b/psalm.xml.dist index 77e6d876..7803297a 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -1,6 +1,7 @@ + + + + + + + + + + + +