diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 818c71bb..23895342 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -80,6 +80,14 @@ jobs: extensions: "${{ matrix.php-extensions }}" ini-values: "zend.assertions=1, max_execution_time=30" + - name: "Install Relay" + run: | + curl -L "https://cachewerk.s3.amazonaws.com/relay/dev/relay-dev-php${{ matrix.php-version }}-debian-x86-64.tar.gz" | tar xz + cd relay-dev-php${{ matrix.php-version }}-debian-x86-64 + sudo cp relay.ini $(php-config --ini-dir) + sudo cp relay-pkg.so $(php-config --extension-dir)/relay.so + sudo sed -i "s/00000000-0000-0000-0000-000000000000/$(cat /proc/sys/kernel/random/uuid)/" $(php-config --extension-dir)/relay.so + - name: "Install symfony/flex" run: "composer require --no-progress --no-scripts --no-plugins symfony/flex" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 0aaca7e8..6d958fd3 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -20,6 +20,14 @@ jobs: coverage: "none" php-version: "7.4" + - name: "Install Relay" + run: | + curl -L "https://cachewerk.s3.amazonaws.com/relay/dev/relay-dev-php7.4-debian-x86-64.tar.gz" | tar xz + cd relay-dev-php7.4-debian-x86-64 + sudo cp relay.ini $(php-config --ini-dir) + sudo cp relay-pkg.so $(php-config --extension-dir)/relay.so + sudo sed -i "s/00000000-0000-0000-0000-000000000000/$(cat /proc/sys/kernel/random/uuid)/" $(php-config --extension-dir)/relay.so + - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v2" diff --git a/docs/README.md b/docs/README.md index f8a9a769..fb4d8198 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ ## About ## -This bundle integrates [Predis](https://github.com/nrk/predis) and [phpredis](https://github.com/nicolasff/phpredis) into your Symfony application. +This bundle integrates [Predis](https://github.com/nrk/predis), [PhpRedis](https://github.com/nicolasff/phpredis) and [Relay](https://relay.so/) into your Symfony application. ## Installation ## @@ -52,7 +52,7 @@ You have to configure at least one client. In the above example your service container will contain the service `snc_redis.default` which will return a `Predis` client. -Available types are `predis` and `phpredis`. +Available types are `predis`, `phpredis` and `relay`. A more complex setup which contains a clustered client could look like this: @@ -104,13 +104,13 @@ snc_redis: Please note that the master dsn connection needs to be tagged with the ```master``` alias. If not, `predis` will complain. -A setup using `predis` or `phpredis` sentinel replication could look like this: +A setup using `predis`, `phpredis` or `relay` sentinel replication could look like this: ``` yaml snc_redis: clients: default: - type: predis + type: "predis" # or "phpredis", or "relay" alias: default dsn: - redis://localhost:26379 @@ -255,12 +255,6 @@ framework: provider: snc_redis.cache ``` -### Profiler storage ### - -:warning: this feature is not supported anymore since Symfony 4.4 and will be automatically disabled if you are using Symfony 4.4. - ->As the profiler must only be used on non-production servers, the file storage is more than enough and no other implementations will ever be supported. - ### Complete configuration example ### ``` yaml diff --git a/src/DependencyInjection/Configuration/Configuration.php b/src/DependencyInjection/Configuration/Configuration.php index 1a8f9d04..739db3a4 100644 --- a/src/DependencyInjection/Configuration/Configuration.php +++ b/src/DependencyInjection/Configuration/Configuration.php @@ -19,6 +19,7 @@ use Predis\Connection\Parameters; use Redis; use RedisCluster; +use Relay\Relay; use Snc\RedisBundle\Client\Predis\Connection\ConnectionFactory; use Snc\RedisBundle\Client\Predis\Connection\ConnectionWrapper; use Snc\RedisBundle\DataCollector\RedisDataCollector; @@ -54,6 +55,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('connection_factory')->defaultValue(ConnectionFactory::class)->end() ->scalarNode('connection_wrapper')->defaultValue(ConnectionWrapper::class)->end() ->scalarNode('phpredis_client')->defaultValue(Redis::class)->end() + ->scalarNode('relay_client')->defaultValue(Relay::class)->end() ->scalarNode('phpredis_clusterclient')->defaultValue(RedisCluster::class)->end() ->scalarNode('logger')->defaultValue(RedisLogger::class)->end() ->scalarNode('data_collector')->defaultValue(RedisDataCollector::class)->end() diff --git a/src/DependencyInjection/SncRedisExtension.php b/src/DependencyInjection/SncRedisExtension.php index ed3df70b..0d07230d 100644 --- a/src/DependencyInjection/SncRedisExtension.php +++ b/src/DependencyInjection/SncRedisExtension.php @@ -115,6 +115,7 @@ private function loadClient(array $client, ContainerBuilder $container): void $this->loadPredisClient($client, $container); break; case 'phpredis': + case 'relay': $this->loadPhpredisClient($client, $container); break; default: @@ -217,7 +218,9 @@ private function loadPhpredisClient(array $options, ContainerBuilder $container) throw new LogicException('You cannot have both cluster and sentinel enabled for same redis connection'); } - $phpredisClientClass = (string) $container->getParameter('snc_redis.phpredis_' . ($hasClusterOption ? 'cluster' : '') . 'client.class'); + $phpredisClientClass = (string) $container->getParameter( + sprintf('snc_redis.%s_%sclient.class', $options['type'], ($hasClusterOption ? 'cluster' : '')), + ); $phpredisDef = new Definition($phpredisClientClass, [ $hasSentinelOption ? RedisSentinel::class : $phpredisClientClass, diff --git a/src/Factory/PhpredisClientFactory.php b/src/Factory/PhpredisClientFactory.php index f7e99169..a17e8f5f 100644 --- a/src/Factory/PhpredisClientFactory.php +++ b/src/Factory/PhpredisClientFactory.php @@ -14,6 +14,8 @@ use RedisSentinel; use ReflectionClass; use ReflectionMethod; +use Relay\Relay; +use Relay\Sentinel; use Snc\RedisBundle\DependencyInjection\Configuration\RedisDsn; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -21,7 +23,7 @@ use function array_keys; use function array_map; use function count; -use function defined; +use function get_class; use function implode; use function in_array; use function is_a; @@ -63,7 +65,7 @@ public function __construct(callable $interceptor, ?Configuration $proxyConfigur * @param list> $dsns Multiple DSN string * @param mixed[] $options Options provided in bundle client config * - * @return Redis|RedisCluster + * @return Redis|RedisCluster|Relay * * @throws InvalidConfigurationException * @throws LogicException @@ -71,11 +73,12 @@ public function __construct(callable $interceptor, ?Configuration $proxyConfigur public function create(string $class, array $dsns, array $options, string $alias, bool $loggingEnabled) { $isRedis = is_a($class, Redis::class, true); - $isSentinel = is_a($class, RedisSentinel::class, true); + $isRelay = is_a($class, Relay::class, true); + $isSentinel = is_a($class, RedisSentinel::class, true) || is_a($class, Sentinel::class, true); $isCluster = is_a($class, RedisCluster::class, true); - if (!$isRedis && !$isSentinel && !$isCluster) { - throw new LogicException(sprintf('The factory can only instantiate Redis|RedisCluster|RedisSentinel classes: "%s" asked', $class)); + if (!$isRedis && !$isRelay && !$isSentinel && !$isCluster) { + throw new LogicException(sprintf('The factory can only instantiate Redis|Relay\Relay|RedisCluster|RedisSentinel|Relay\Sentinel classes: "%s" asked', $class)); } // Normalize the DSNs, because using processed environment variables could lead to nested values. @@ -83,7 +86,7 @@ public function create(string $class, array $dsns, array $options, string $alias $parsedDsns = array_map(static fn (string $dsn) => new RedisDsn($dsn), $dsns); - if ($isRedis) { + if ($isRedis || $isRelay) { if (count($parsedDsns) > 1) { throw new LogicException('Cannot have more than 1 dsn with \Redis and \RedisArray is not supported yet.'); } @@ -92,20 +95,26 @@ public function create(string $class, array $dsns, array $options, string $alias } if ($isSentinel) { - return $this->createClientFromSentinel($parsedDsns, $alias, $options, $loggingEnabled); + return $this->createClientFromSentinel($class, $parsedDsns, $alias, $options, $loggingEnabled); } return $this->createClusterClient($parsedDsns, $class, $alias, $options, $loggingEnabled); } /** + * @param class-string $class * @param list $dsns * @param array{service: ?string} $options + * + * @return Redis|Relay */ - private function createClientFromSentinel(array $dsns, string $alias, array $options, bool $loggingEnabled): Redis + private function createClientFromSentinel(string $class, array $dsns, string $alias, array $options, bool $loggingEnabled) { + $isRelay = is_a($class, Sentinel::class, true); + $sentinelClass = $isRelay ? Sentinel::class : RedisSentinel::class; + foreach ($dsns as $dsn) { - $address = (new RedisSentinel($dsn->getHost(), (int) $dsn->getPort()))->getMasterAddrByName($options['service']); + $address = (new $sentinelClass($dsn->getHost(), (int) $dsn->getPort()))->getMasterAddrByName($options['service']); if (!$address) { continue; @@ -120,7 +129,7 @@ public function __construct(string $dsn, string $host, int $port) $this->port = $port; } }, - Redis::class, + $isRelay ? Relay::class : Redis::class, $alias, $options, $loggingEnabled, @@ -155,7 +164,7 @@ private function createClusterClient(array $dsns, string $class, string $alias, ); if (isset($options['prefix'])) { - $client->setOption(Redis::OPT_PREFIX, $options['prefix']); + $client->setOption(2, $options['prefix']); } if (isset($options['serialization'])) { @@ -169,8 +178,12 @@ private function createClusterClient(array $dsns, string $class, string $alias, return $loggingEnabled ? $this->createLoggingProxy($client, $alias) : $client; } - /** @param mixed[] $options */ - private function createClient(RedisDsn $dsn, string $class, string $alias, array $options, bool $loggingEnabled): Redis + /** + * @param mixed[] $options + * + * @return Redis|Relay + */ + private function createClient(RedisDsn $dsn, string $class, string $alias, array $options, bool $loggingEnabled) { $client = new $class(); @@ -229,24 +242,18 @@ private function createClient(RedisDsn $dsn, string $class, string $alias, array return $client; } - /** - * @return Redis::SERIALIZER_* - * - * @throws InvalidConfigurationException - */ + /** @throws InvalidConfigurationException */ private function loadSerializationType(string $type): int { $types = [ - 'default' => Redis::SERIALIZER_NONE, - 'json' => Redis::SERIALIZER_JSON, - 'none' => Redis::SERIALIZER_NONE, - 'php' => Redis::SERIALIZER_PHP, + 'default' => 0, // Redis::SERIALIZER_NONE, + 'none' => 0, // Redis::SERIALIZER_NONE, + 'php' => 1, //Redis::SERIALIZER_PHP, + 'igbinary' => 2, //Redis::SERIALIZER_IGBINARY, + 'msgpack' => 3, //Redis::SERIALIZER_MSGPACK, + 'json' => 4, // Redis::SERIALIZER_JSON, ]; - if (defined('Redis::SERIALIZER_IGBINARY')) { - $types['igbinary'] = Redis::SERIALIZER_IGBINARY; - } - if (array_key_exists($type, $types)) { return $types[$type]; } @@ -275,14 +282,13 @@ private function loadSlaveFailoverType(string $type): int * * @return T * - * @template T of Redis|RedisCluster + * @template T of Redis|Relay|RedisCluster */ private function createLoggingProxy(object $client, string $alias): object { - $prefixInterceptors = []; - $classToCopyMethodsFrom = $client instanceof Redis ? Redis::class : RedisCluster::class; + $prefixInterceptors = []; - foreach ((new ReflectionClass($classToCopyMethodsFrom))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + foreach ((new ReflectionClass(get_class($client)))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { $name = $method->getName(); if ($name[0] === '_' || in_array($name, self::CLIENT_ONLY_COMMANDS, true)) { diff --git a/tests/DependencyInjection/Fixtures/config/yaml/env_relay_minimal.yaml b/tests/DependencyInjection/Fixtures/config/yaml/env_relay_minimal.yaml new file mode 100644 index 00000000..d5c16341 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/config/yaml/env_relay_minimal.yaml @@ -0,0 +1,9 @@ +parameters: + env(REDIS_URL): redis://localhost + +snc_redis: + clients: + default: + type: relay + alias: default + dsn: "%env(REDIS_URL)%" diff --git a/tests/DependencyInjection/SncRedisExtensionEnvTest.php b/tests/DependencyInjection/SncRedisExtensionEnvTest.php index 3683f63f..86972d76 100644 --- a/tests/DependencyInjection/SncRedisExtensionEnvTest.php +++ b/tests/DependencyInjection/SncRedisExtensionEnvTest.php @@ -40,14 +40,18 @@ public function testPredisDefaultParameterWithSSLContextConfigLoad(): void ); } - public function testPhpredisDefaultParameterConfig(): void + /** + * @testWith ["env_phpredis_minimal", "Redis"] + * ["env_relay_minimal", "Relay\\Relay"] + */ + public function testPhpredisDefaultParameterConfig(string $config, string $class): void { - $container = $this->getConfiguredContainer('env_phpredis_minimal'); + $container = $this->getConfiguredContainer($config); $clientDefinition = $container->findDefinition('snc_redis.default'); - $this->assertSame(Redis::class, $clientDefinition->getClass()); - $this->assertSame(Redis::class, $clientDefinition->getArgument(0)); + $this->assertSame($class, $clientDefinition->getClass()); + $this->assertSame($class, $clientDefinition->getArgument(0)); $this->assertStringContainsString('REDIS_URL', $clientDefinition->getArgument(1)[0]); $this->assertSame('default', $clientDefinition->getArgument(3)); diff --git a/tests/DependencyInjection/SncRedisExtensionTest.php b/tests/DependencyInjection/SncRedisExtensionTest.php index 9237b036..be8ad137 100644 --- a/tests/DependencyInjection/SncRedisExtensionTest.php +++ b/tests/DependencyInjection/SncRedisExtensionTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; use Redis; use RedisException; +use Relay\Relay; use Snc\RedisBundle\DependencyInjection\Configuration\Configuration; use Snc\RedisBundle\DependencyInjection\SncRedisExtension; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -43,6 +44,7 @@ public static function parameterValues(): array { return [ ['snc_redis.client.class', 'Predis\Client'], + ['snc_redis.relay_client.class', Relay::class], ['snc_redis.client_options.class', 'Predis\Configuration\Options'], ['snc_redis.connection_parameters.class', 'Predis\Connection\Parameters'], ['snc_redis.connection_factory.class', 'Snc\RedisBundle\Client\Predis\Connection\ConnectionFactory'], diff --git a/tests/Factory/PhpredisClientFactoryTest.php b/tests/Factory/PhpredisClientFactoryTest.php index e6f4b1db..8f90e1fd 100644 --- a/tests/Factory/PhpredisClientFactoryTest.php +++ b/tests/Factory/PhpredisClientFactoryTest.php @@ -9,7 +9,7 @@ use Psr\Log\LoggerInterface; use Redis; use RedisCluster; -use RedisSentinel; +use Relay\Relay; use Snc\RedisBundle\Factory\PhpredisClientFactory; use Snc\RedisBundle\Logger\RedisCallInterceptor; use Snc\RedisBundle\Logger\RedisLogger; @@ -51,6 +51,19 @@ public function testCreateMinimalConfig(): void $this->assertNull($client->getPersistentID()); } + /** @requires extension relay */ + public function testCreateRelay(): void + { + $this->logger->method('debug')->withConsecutive( + [$this->stringContains('Executing command "CONNECT localhost 6379 5 ')], + ); + + $client = (new PhpredisClientFactory(new RedisCallInterceptor($this->redisLogger))) + ->create(Relay::class, ['redis://localhost:6379'], ['connection_timeout' => 5], 'default', true); + + $this->assertInstanceOf(Relay::class, $client); + } + public function testUnixDsnConfig(): void { $this->logger->expects($this->never())->method('debug'); @@ -87,7 +100,12 @@ public function testCreateMinimalClusterConfig(): void $this->assertSame(0, $client->getOption(RedisCluster::OPT_SLAVE_FAILOVER)); } - public function testCreatSentinelConfig(): void + /** + * @requires extension relay + * @testWith ["RedisSentinel", "Redis"] + * ["Relay\\Sentinel", "Relay\\Relay"] + */ + public function testCreateSentinelConfig(string $sentinelClass, string $outputClass): void { $this->logger->method('debug')->withConsecutive( [$this->stringContains('Executing command "CONNECT 127.0.0.1 6379 5 ')], @@ -96,14 +114,14 @@ public function testCreatSentinelConfig(): void $factory = new PhpredisClientFactory(new RedisCallInterceptor($this->redisLogger)); $client = $factory->create( - RedisSentinel::class, + $sentinelClass, ['redis://sncredis@localhost:26379'], ['connection_timeout' => 5, 'connection_persistent' => false, 'service' => 'mymaster'], 'phpredissentinel', true, ); - $this->assertInstanceOf(Redis::class, $client); + $this->assertInstanceOf($outputClass, $client); $this->assertNull($client->getOption(Redis::OPT_PREFIX)); $this->assertSame(0, $client->getOption(Redis::OPT_SERIALIZER)); $this->assertSame('sncredis', $client->getAuth());