Commit 0d2f5028 by Carsten Brandt

Finished Redis Connection class

parent 8e74add1
......@@ -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 {
$dsn = explode('/', $this->dsn);
$host = $dsn[2];
if (strpos($host, ':')===false) {
$host .= ':6379';
}
$db = isset($dsn[3]) ? $dsn[3] : 0;
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
$this->_socket = stream_socket_client($host . ':' . $port);
// TODO auth
// TODO select database
$this->initConnection();
$this->_socket = @stream_socket_client($host, $errorNumber, $errorDescription);
if ($this->_socket) {
if ($this->password !== null) {
$this->executeCommand('AUTH', array($this->password));
}
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);
$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,21 +321,37 @@ class Connection extends Component
*/
public function __call($name, $params)
{
// TODO set active to true?
if (in_array($name, $this->redisCommands))
$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: {$command}", __CLASS__);
\Yii::trace("Executing Redis Command: {$name}", __CLASS__);
fwrite($this->_socket, $command);
return $this->parseResponse($command);
}
else {
return parent::__call($name, $params);
}
return $this->parseResponse(implode(' ', $params));
}
private function parseResponse($command)
......@@ -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);
}
}
......@@ -7,4 +7,9 @@ return array(
'password' => '',
'fixture' => __DIR__ . '/mysql.sql',
),
'redis' => array(
'dsn' => 'redis://localhost:6379/0',
'password' => null,
// 'fixture' => __DIR__ . '/mysql.sql',
),
);
......@@ -4,19 +4,44 @@ namespace yiiunit\framework\db\redis;
use yii\db\redis\Connection;
/**
*
*
* @author Carsten Brandt <mail@cebe.cc>
*/
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
<?php
/**
*
*
* @author Carsten Brandt <mail@cebe.cc>
*/
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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment