diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php index a4c18c6255..1bdb1685cd 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -18,6 +18,13 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException; */ class RedisAdapter extends AbstractAdapter { + private static $defaultConnectionOptions = array( + 'class' => \Redis::class, + 'persistent' => 0, + 'timeout' => 0, + 'read_timeout' => 0, + 'retry_interval' => 0, + ); private $redis; public function __construct(\Redis $redisConnection, $namespace = '', $defaultLifetime = 0) @@ -30,6 +37,80 @@ class RedisAdapter extends AbstractAdapter parent::__construct($namespace, $defaultLifetime); } + /** + * Creates a Redis connection using a DSN configuration. + * + * Example DSN: + * - redis://localhost + * - redis://example.com:1234 + * - redis://secret@example.com/13 + * - redis:///var/run/redis.sock + * - redis://secret@/var/run/redis.sock/13 + * + * @param string $dsn + * @param array $options See self::$defaultConnectionOptions + * + * @throws InvalidArgumentException When the DSN is invalid. + * + * @return \Redis + */ + public static function createConnection($dsn, array $options = array()) + { + if (0 !== strpos($dsn, 'redis://')) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn)); + } + $params = preg_replace_callback('#^redis://(?:([^@]*)@)?#', function ($m) use (&$auth) { + if (isset($m[1])) { + $auth = $m[1]; + } + + return 'file://'; + }, $dsn); + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn)); + } + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn)); + } + if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['dbindex'] = $m[1]; + $params['path'] = substr($params['path'], 0, -strlen($m[0])); + } + $params += array( + 'host' => isset($params['host']) ? $params['host'] : $params['path'], + 'port' => isset($params['host']) ? 6379 : null, + 'dbindex' => 0, + ); + if (isset($params['query'])) { + parse_str($params['query'], $query); + $params += $query; + } + $params += $options + self::$defaultConnectionOptions; + + if (\Redis::class !== $params['class'] && !is_subclass_of($params['class'], \Redis::class)) { + throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis"', $params['class'])); + } + $connect = empty($params['persistent']) ? 'connect' : 'pconnect'; + $redis = new $params['class'](); + @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], null, $params['retry_interval']); + + if (@!$redis->isConnected()) { + $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : ''; + throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn)); + } + + if ((null !== $auth && !$redis->auth($auth)) + || ($params['dbindex'] && !$redis->select($params['dbindex'])) + || ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout'])) + ) { + $e = preg_replace('/^ERR /', '', $redis->getLastError()); + $redis->close(); + throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn)); + } + + return $redis; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php index fe3eaead39..f8404b89e5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php @@ -44,4 +44,60 @@ class RedisAdapterTest extends CachePoolTest self::$redis->flushDB(); self::$redis->close(); } + + public function testCreateConnection() + { + $redis = RedisAdapter::createConnection('redis://localhost'); + $this->assertTrue($redis->isConnected()); + $this->assertSame(0, $redis->getDbNum()); + + $redis = RedisAdapter::createConnection('redis://localhost/2'); + $this->assertSame(2, $redis->getDbNum()); + + $redis = RedisAdapter::createConnection('redis://localhost', array('timeout' => 3)); + $this->assertEquals(3, $redis->getTimeout()); + + $redis = RedisAdapter::createConnection('redis://localhost?timeout=4'); + $this->assertEquals(4, $redis->getTimeout()); + + $redis = RedisAdapter::createConnection('redis://localhost', array('read_timeout' => 5)); + $this->assertEquals(5, $redis->getReadTimeout()); + } + + /** + * @dataProvider provideFailedCreateConnection + * @expectedException Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Redis connection failed + */ + public function testFailedCreateConnection($dsn) + { + RedisAdapter::createConnection($dsn); + } + + public function provideFailedCreateConnection() + { + return array( + array('redis://localhost:1234'), + array('redis://foo@localhost'), + array('redis://localhost/123'), + ); + } + + /** + * @dataProvider provideInvalidCreateConnection + * @expectedException Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid Redis DSN + */ + public function testInvalidCreateConnection($dsn) + { + RedisAdapter::createConnection($dsn); + } + + public function provideInvalidCreateConnection() + { + return array( + array('foo://localhost'), + array('redis://'), + ); + } }