From 0d2f5028ef1b33f54f91d7e2508e574a20cd849d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 29 Mar 2013 01:02:35 +0100 Subject: [PATCH] Finished Redis Connection class --- framework/db/redis/Connection.php | 141 ++++++++++------------- tests/unit/data/config.php | 5 + tests/unit/framework/db/redis/ConnectionTest.php | 47 ++++++-- tests/unit/framework/db/redis/RedisTestCase.php | 36 +++--- 4 files changed, 125 insertions(+), 104 deletions(-) diff --git a/framework/db/redis/Connection.php b/framework/db/redis/Connection.php index 00525d9..96ab288 100644 --- a/framework/db/redis/Connection.php +++ b/framework/db/redis/Connection.php @@ -10,14 +10,17 @@ namespace yii\db\redis; use \yii\base\Component; -use yii\base\NotSupportedException; use yii\base\InvalidConfigException; use \yii\db\Exception; +use yii\util\StringHelper; /** * * * + * @method mixed set($key, $value) Set the string value of a key + * @method mixed get($key) Set the string value of a key + * TODO document methods * * @since 2.0 */ @@ -30,34 +33,18 @@ class Connection extends Component /** * @var string the Data Source Name, or DSN, contains the information required to connect to the database. - * DSN format: redis://[auth@][server][:port][/db] - * @see charset + * DSN format: redis://server:port[/db] + * Where db is a zero based integer which refers to the DB to use. + * If no DB is given, ID 0 is used. + * + * Example: redis://localhost:6379/2 */ public $dsn; /** - * @var string the username for establishing DB connection. Defaults to empty string. - */ - public $username = ''; - /** - * @var string the password for establishing DB connection. Defaults to empty string. - */ - public $password = ''; - /** - * @var boolean whether to enable profiling for the SQL statements being executed. - * Defaults to false. This should be mainly enabled and used during development - * to find out the bottleneck of SQL executions. - * @see getStats + * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send. + * See http://redis.io/commands/auth */ - public $enableProfiling = false; - /** - * @var string the common prefix or suffix for table names. If a table name is given - * as `{{%TableName}}`, then the percentage character `%` will be replaced with this - * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is - * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]] - * is true. - * @see enableAutoQuoting - */ - public $keyPrefix; + public $password; /** * @var array List of available redis commands http://redis.io/commands @@ -242,20 +229,25 @@ class Connection extends Component if (empty($this->dsn)) { throw new InvalidConfigException('Connection.dsn cannot be empty.'); } - // TODO parse DSN - $host = 'localhost'; - $port = 6379; - try { - \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); - $this->_socket = stream_socket_client($host . ':' . $port); - // TODO auth - // TODO select database - $this->initConnection(); + $dsn = explode('/', $this->dsn); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':6379'; } - catch (\PDOException $e) { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); - $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; - throw new Exception($message, (int)$e->getCode(), $e->errorInfo); + $db = isset($dsn[3]) ? $dsn[3] : 0; + + \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + $this->_socket = @stream_socket_client($host, $errorNumber, $errorDescription); + if ($this->_socket) { + if ($this->password !== null) { + $this->executeCommand('AUTH', array($this->password)); + } + $this->executeCommand('SELECT', array($db)); + $this->initConnection(); + } else { + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); + $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; + throw new Exception($message, (int)$errorNumber, $errorDescription); } } } @@ -268,7 +260,7 @@ class Connection extends Component { if ($this->_socket !== null) { \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); - $this->__call('CLOSE', array()); // TODO improve API + $this->executeCommand('QUIT'); stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); $this->_socket = null; $this->_transaction = null; @@ -278,9 +270,7 @@ class Connection extends Component /** * Initializes the DB connection. * This method is invoked right after the DB connection is established. - * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES` - * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty. - * It then triggers an [[EVENT_AFTER_OPEN]] event. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. */ protected function initConnection() { @@ -324,8 +314,6 @@ class Connection extends Component } /** - * http://redis.io/topics/protocol - * https://github.com/ptrofimov/tinyredisclient/blob/master/src/TinyRedisClient.php * * @param string $name * @param array $params @@ -333,23 +321,39 @@ class Connection extends Component */ public function __call($name, $params) { - // TODO set active to true? - if (in_array($name, $this->redisCommands)) - { - array_unshift($params, $name); - $command = '*' . count($params) . "\r\n"; - foreach($params as $arg) { - $command .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n"; - } - \Yii::trace("Executing Redis Command: {$command}", __CLASS__); - fwrite($this->_socket, $command); - return $this->parseResponse($command); - } - else { + $redisCommand = strtoupper(StringHelper::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) { + return $this->executeCommand($name, $params); + } else { return parent::__call($name, $params); } } + /** + * Execute a redis command + * http://redis.io/commands + * http://redis.io/topics/protocol + * + * @param $name + * @param $params + * @return array|bool|null|string + */ + public function executeCommand($name, $params=array()) + { + $this->open(); + + array_unshift($params, $name); + $command = '*' . count($params) . "\r\n"; + foreach($params as $arg) { + $command .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n"; + } + + \Yii::trace("Executing Redis Command: {$name}", __CLASS__); + fwrite($this->_socket, $command); + + return $this->parseResponse(implode(' ', $params)); + } + private function parseResponse($command) { if(($line = fgets($this->_socket))===false) { @@ -383,28 +387,7 @@ class Connection extends Component } return $data; default: - throw new Exception('Received illegal data from redis: ' . substr($line, 0, -2) . "\nRedis command was: " . $command); - } - } - - /** - * Returns the statistical results of SQL queries. - * The results returned include the number of SQL statements executed and - * the total time spent. - * In order to use this method, [[enableProfiling]] has to be set true. - * @return array the first element indicates the number of SQL statements executed, - * and the second element the total time spent in SQL execution. - * @see \yii\logging\Logger::getProfiling() - */ - public function getQuerySummary() - { - $logger = \Yii::getLogger(); - $timings = $logger->getProfiling(array('yii\db\redis\Connection::command')); - $count = count($timings); - $time = 0; - foreach ($timings as $timing) { - $time += $timing[1]; + throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); } - return array($count, $time); } } diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index fc15690..2640696 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -7,4 +7,9 @@ return array( 'password' => '', 'fixture' => __DIR__ . '/mysql.sql', ), + 'redis' => array( + 'dsn' => 'redis://localhost:6379/0', + 'password' => null, +// 'fixture' => __DIR__ . '/mysql.sql', + ), ); diff --git a/tests/unit/framework/db/redis/ConnectionTest.php b/tests/unit/framework/db/redis/ConnectionTest.php index 904f1e6..ab66e1d 100644 --- a/tests/unit/framework/db/redis/ConnectionTest.php +++ b/tests/unit/framework/db/redis/ConnectionTest.php @@ -4,19 +4,44 @@ namespace yiiunit\framework\db\redis; use yii\db\redis\Connection; -/** - * - * - * @author Carsten Brandt - */ class ConnectionTest extends RedisTestCase { - public function testConstruct() + /** + * Empty DSN should throw exception + * @expectedException \yii\base\InvalidConfigException + */ + public function testEmptyDSN() + { + $db = new Connection(); + $db->open(); + } + + /** + * test connection to redis and selection of db + */ + public function testConnect() { $db = new Connection(); + $db->dsn = 'redis://localhost:6379'; + $db->open(); + $this->assertTrue($db->ping()); + $db->set('YIITESTKEY', 'YIITESTVALUE'); + $db->close(); + + $db = new Connection(); + $db->dsn = 'redis://localhost:6379/0'; + $db->open(); + $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); + $db->close(); + + $db = new Connection(); + $db->dsn = 'redis://localhost:6379/1'; + $db->open(); + $this->assertNull($db->get('YIITESTKEY')); + $db->close(); } - public function storeGetData() + public function keyValueData() { return array( array(123), @@ -24,18 +49,18 @@ class ConnectionTest extends RedisTestCase array(0), array('test'), array("test\r\ntest"), - array(json_encode($this)), + array(''), ); } /** - * @dataProvider storeGetData + * @dataProvider keyValueData */ public function testStoreGet($data) { $db = $this->getConnection(true); - $db->SET('hi', $data); - $this->assertEquals($data, $db->GET('hi')); + $db->set('hi', $data); + $this->assertEquals($data, $db->get('hi')); } } \ No newline at end of file diff --git a/tests/unit/framework/db/redis/RedisTestCase.php b/tests/unit/framework/db/redis/RedisTestCase.php index eef8c84..7c9ee3e 100644 --- a/tests/unit/framework/db/redis/RedisTestCase.php +++ b/tests/unit/framework/db/redis/RedisTestCase.php @@ -1,39 +1,47 @@ - */ namespace yiiunit\framework\db\redis; - use yii\db\redis\Connection; use yiiunit\TestCase; +/** + * RedisTestCase is the base class for all redis related test cases + */ class RedisTestCase extends TestCase { - function __construct() + protected function setUp() { - // TODO check if a redis server is running - //$this->markTestSkipped('No redis server running at port ...'); + $params = $this->getParam('redis'); + if ($params === null || !isset($params['dsn'])) { + $this->markTestSkipped('No redis server connection configured.'); + } + $dsn = explode('/', $params['dsn']); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':6379'; + } + if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + parent::setUp(); } /** * @param bool $reset whether to clean up the test database * @return Connection */ - function getConnection($reset = true) + public function getConnection($reset = true) { $params = $this->getParam('redis'); $db = new \yii\db\redis\Connection; $db->dsn = $params['dsn']; - $db->username = $params['username']; $db->password = $params['password']; if ($reset) { - // TODO implement -/* $db->open(); - $lines = explode(';', file_get_contents($params['fixture'])); + $db->open(); + $db->flushall(); +/* $lines = explode(';', file_get_contents($params['fixture'])); foreach ($lines as $line) { if (trim($line) !== '') { $db->pdo->exec($line);