Skip to content

Commit 3b676a1

Browse files
committed
feat: implement redis array on phpredis. Issue #195
1 parent 3d9061f commit 3b676a1

File tree

6 files changed

+199
-7
lines changed

6 files changed

+199
-7
lines changed

docs/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,33 @@ framework:
295295
provider: snc_redis.cache
296296
```
297297

298+
### RedisArray Support ###
299+
300+
The bundle supports RedisArray for phpredis clients, allowing data distribution across multiple Redis instances using the same client class as single connections.
301+
302+
Example configuration:
303+
304+
```yaml
305+
snc_redis:
306+
clients:
307+
default:
308+
type: phpredis
309+
alias: default
310+
dsns:
311+
- redis://localhost:6379
312+
- redis://localhost:6380
313+
options:
314+
connection_timeout: 5
315+
retry_interval: 100
316+
parameters:
317+
database: 0
318+
password: mypassword
319+
redis_array: true
320+
```
321+
322+
This configuration creates a RedisArray client for multiple Redis instances.
323+
324+
298325
### Complete configuration example ###
299326

300327
``` yaml

src/DependencyInjection/Configuration/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ private function addClientsSection(ArrayNodeDefinition $rootNode): void
134134
->end()
135135
->end()
136136
->scalarNode('connection_timeout')->cannotBeEmpty()->defaultValue(5)->end()
137+
->scalarNode('scan')->defaultNull()->end()
137138
->scalarNode('read_write_timeout')->defaultNull()->end()
138139
->booleanNode('iterable_multibulk')->defaultFalse()->end()
139140
->booleanNode('throw_errors')->defaultTrue()->end()
@@ -166,6 +167,7 @@ private function addClientsSection(ArrayNodeDefinition $rootNode): void
166167
->scalarNode('sentinel_username')->defaultNull()->end()
167168
->scalarNode('sentinel_password')->defaultNull()->end()
168169
->booleanNode('logging')->defaultValue($this->debug)->end()
170+
->booleanNode('redis_array')->defaultFalse()->end()
169171
->variableNode('ssl_context')->defaultNull()->end()
170172
->end()
171173
->end()

src/DependencyInjection/SncRedisExtension.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,13 @@ private function loadPredisConnectionParameters(string $clientAlias, array $opti
227227
/** @param mixed[] $options A client configuration */
228228
private function loadPhpredisClient(array $options, ContainerBuilder $container): void
229229
{
230-
$connectionCount = count($options['dsns']);
231-
$hasClusterOption = $options['options']['cluster'] !== null;
232-
$hasSentinelOption = isset($options['options']['replication']);
230+
$connectionCount = count($options['dsns']);
231+
$hasClusterOption = $options['options']['cluster'] !== null;
232+
$hasSentinelOption = isset($options['options']['replication']);
233+
$hasRedisArrayOption = $client['options']['redis_array'] ?? false;
233234

234-
if ($connectionCount > 1 && !$hasClusterOption && !$hasSentinelOption) {
235-
throw new LogicException('Use options "cluster" or "sentinel" to enable support for multi DSN instances.');
235+
if ($connectionCount > 1 && !$hasClusterOption && !$hasSentinelOption && !$hasRedisArrayOption) {
236+
throw new LogicException('Use options "cluster", "replication", or "redis_array" to enable support for multi DSN instances.');
236237
}
237238

238239
if ($hasClusterOption && $hasSentinelOption) {

src/Factory/PhpredisClientFactory.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use ProxyManager\Factory\AccessInterceptorValueHolderFactory;
1111
use ProxyManager\Proxy\AccessInterceptorInterface;
1212
use Redis;
13+
use RedisArray;
1314
use RedisCluster;
1415
use RedisException;
1516
use RedisSentinel;
@@ -75,7 +76,7 @@ public function __construct(callable $interceptor, ?Configuration $proxyConfigur
7576
* @param list<string|list<string>> $dsns Multiple DSN string
7677
* @param mixed[] $options Options provided in bundle client config
7778
*
78-
* @return Redis|RedisCluster|Relay
79+
* @return Redis|RedisCluster|Relay|RedisArray
7980
*
8081
* @throws InvalidConfigurationException
8182
* @throws LogicException
@@ -96,6 +97,18 @@ public function create(string $class, array $dsns, array $options, string $alias
9697

9798
$parsedDsns = array_map(static fn (string $dsn) => new RedisDsn($dsn), $dsns);
9899

100+
if ($options['redis_array'] ?? false) {
101+
if ($isSentinel || $isCluster) {
102+
throw new LogicException('The redis_array option is only supported for Redis or Relay classes.');
103+
}
104+
105+
if ($options['cluster'] || $options['replication']) {
106+
throw new LogicException('The redis_array option cannot be combined with cluster or replication options.');
107+
}
108+
109+
return $this->createRedisArrayClient($parsedDsns, $class, $alias, $options, $loggingEnabled);
110+
}
111+
99112
if ($isRedis || $isRelay) {
100113
if (count($parsedDsns) > 1) {
101114
throw new LogicException('Cannot have more than 1 dsn with \Redis and \RedisArray is not supported yet.');
@@ -111,6 +124,58 @@ public function create(string $class, array $dsns, array $options, string $alias
111124
return $this->createClusterClient($parsedDsns, $class, $alias, $options, $loggingEnabled);
112125
}
113126

127+
/**
128+
* @param RedisDsn[] $dsns
129+
* @param mixed[] $options
130+
*/
131+
private function createRedisArrayClient(array $dsns, string $class, string $alias, array $options, bool $loggingEnabled): RedisArray
132+
{
133+
if (count($dsns) < 2) {
134+
throw new LogicException('The redis_array option requires at least two DSNs.');
135+
}
136+
137+
$hosts = array_map(static fn (RedisDsn $dsn) => ($dsn->getTls() ? 'tls://' : '') . $dsn->getHost() . ':' . $dsn->getPort(), $dsns);
138+
139+
$redisArrayOptions = [
140+
'connect_timeout' => (float) ($options['connection_timeout'] ?? 5),
141+
'retry_interval' => (int) ($options['retry_interval'] ?? 100),
142+
];
143+
144+
$username = $options['parameters']['username'] ?? null;
145+
$password = $options['parameters']['password'] ?? null;
146+
if ($username !== null && $password !== null) {
147+
$redisArrayOptions['auth'] = [$username, $password];
148+
} elseif ($password !== null) {
149+
$redisArrayOptions['auth'] = $password;
150+
}
151+
152+
try {
153+
$client = new RedisArray($hosts, $redisArrayOptions);
154+
} catch (RedisException $e) {
155+
throw new RedisException(sprintf('Failed to create RedisArray with hosts %s: %s', implode(', ', $hosts), $e->getMessage()), 0, $e);
156+
}
157+
158+
$connectedHosts = $client->getHosts();
159+
if (empty($connectedHosts)) {
160+
throw new LogicException(sprintf('RedisArray failed to connect to any hosts: %s', implode(', ', $hosts)));
161+
}
162+
163+
$db = $options['parameters']['database'] ?? null;
164+
if ($db !== null && $db !== '') {
165+
$client->select($db);
166+
}
167+
168+
if (isset($options['prefix'])) {
169+
$client->setOption(Redis::OPT_PREFIX, $options['prefix']);
170+
}
171+
172+
if (isset($options['serialization'])) {
173+
$client->setOption(Redis::OPT_SERIALIZER, $this->loadSerializationType($options['serialization']));
174+
}
175+
176+
return $loggingEnabled ? $this->createLoggingProxy($client, $alias) : $client;
177+
}
178+
114179
/**
115180
* @param class-string $class
116181
* @param list<RedisDsn> $dsns

tests/Factory/PhpredisClientFactoryTest.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
use PHPUnit\Framework\TestCase;
99
use Psr\Log\LoggerInterface;
1010
use Redis;
11+
use RedisArray;
1112
use RedisCluster;
13+
use RedisSentinel;
1214
use Relay\Relay;
1315
use SEEC\PhpUnit\Helper\ConsecutiveParams;
16+
use Snc\RedisBundle\DependencyInjection\Configuration\RedisDsn;
1417
use Snc\RedisBundle\Factory\PhpredisClientFactory;
1518
use Snc\RedisBundle\Logger\RedisCallInterceptor;
1619
use Snc\RedisBundle\Logger\RedisLogger;
1720
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1821

22+
use function array_map;
1923
use function class_exists;
2024
use function fsockopen;
2125
use function phpversion;
@@ -468,4 +472,93 @@ public function testCreateWithConnectionPersistentFalse(): void
468472
$this->assertInstanceOf(Redis::class, $client);
469473
$this->assertNull($client->getPersistentID());
470474
}
475+
476+
public function testCreateRedisArrayClient(): void
477+
{
478+
$dsns = [
479+
new RedisDsn('redis://localhost:6379'),
480+
new RedisDsn('redis://localhost:6380'),
481+
];
482+
$options = [
483+
'redis_array' => true,
484+
'connection_timeout' => 5,
485+
'retry_interval' => 100,
486+
'parameters' => ['database' => 0, 'password' => 'mypassword'],
487+
];
488+
$factory = new PhpredisClientFactory(static function (): void {
489+
}, null);
490+
$client = $factory->create(
491+
Redis::class,
492+
array_map('strval', $dsns),
493+
$options,
494+
'default',
495+
false,
496+
);
497+
498+
$this->assertInstanceOf(RedisArray::class, $client);
499+
$this->assertEquals(['localhost:6379', 'localhost:6380'], $client->getHosts());
500+
}
501+
502+
public function testThrowsExceptionForMultipleDsnsWithoutValidOption(): void
503+
{
504+
$this->expectException(LogicException::class);
505+
$this->expectExceptionMessage('Cannot have more than 1 dsn with \Redis and \RedisArray is not supported yet.');
506+
507+
$dsns = [
508+
new RedisDsn('redis://localhost:6379'),
509+
new RedisDsn('redis://localhost:6380'),
510+
];
511+
$options = [];
512+
$factory = new PhpredisClientFactory(static function (): void {
513+
}, null);
514+
$factory->create(
515+
Redis::class,
516+
array_map('strval', $dsns),
517+
$options,
518+
'default',
519+
false,
520+
);
521+
}
522+
523+
public function testThrowsExceptionForRedisArrayWithClusterOrReplication(): void
524+
{
525+
$this->expectException(LogicException::class);
526+
$this->expectExceptionMessage('The redis_array option cannot be combined with cluster or replication options.');
527+
528+
$dsns = [
529+
new RedisDsn('redis://localhost:6379'),
530+
new RedisDsn('redis://localhost:6380'),
531+
];
532+
$options = ['redis_array' => true, 'cluster' => true];
533+
$factory = new PhpredisClientFactory(static function (): void {
534+
}, null);
535+
$factory->create(
536+
Redis::class,
537+
array_map('strval', $dsns),
538+
$options,
539+
'default',
540+
false,
541+
);
542+
}
543+
544+
public function testThrowsExceptionForRedisArrayWithSentinel(): void
545+
{
546+
$this->expectException(\LogicException::class);
547+
$this->expectExceptionMessage('The redis_array option is only supported for Redis or Relay classes.');
548+
549+
$dsns = [
550+
new RedisDsn('redis://localhost:6379'),
551+
new RedisDsn('redis://localhost:6380'),
552+
];
553+
$options = ['redis_array' => true];
554+
$factory = new PhpredisClientFactory(static function (): void {
555+
}, null);
556+
$factory->create(
557+
RedisSentinel::class,
558+
array_map('strval', $dsns),
559+
$options,
560+
'default',
561+
false,
562+
);
563+
}
471564
}

tests/Functional/App/config.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ snc_redis:
1919
default:
2020
type: phpredis
2121
alias: default
22-
dsn: redis://sncredis@localhost
22+
dsns:
23+
- redis://sncredis@localhost/1?role=master
24+
- redis://sncredis@localhost/2
2325
logging: '%kernel.debug%'
26+
options:
27+
redis_array: true
2428
cache:
2529
type: predis
2630
alias: cache

0 commit comments

Comments
 (0)