Browse Source

Finished Redis Connection class

tags/2.0.0-beta
Carsten Brandt 12 years ago
parent
commit
0d2f5028ef
  1. 129
      framework/db/redis/Connection.php
  2. 5
      tests/unit/data/config.php
  3. 45
      tests/unit/framework/db/redis/ConnectionTest.php
  4. 36
      tests/unit/framework/db/redis/RedisTestCase.php

129
framework/db/redis/Connection.php

@ -10,14 +10,17 @@
namespace yii\db\redis; namespace yii\db\redis;
use \yii\base\Component; use \yii\base\Component;
use yii\base\NotSupportedException;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use \yii\db\Exception; 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 * @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. * @var string the Data Source Name, or DSN, contains the information required to connect to the database.
* DSN format: redis://[auth@][server][:port][/db] * DSN format: redis://server:port[/db]
* @see charset * 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; public $dsn;
/** /**
* @var string the username for establishing DB connection. Defaults to empty string. * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send.
*/ * See http://redis.io/commands/auth
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
*/ */
public $enableProfiling = false; public $password;
/**
* @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;
/** /**
* @var array List of available redis commands http://redis.io/commands * @var array List of available redis commands http://redis.io/commands
@ -242,20 +229,25 @@ class Connection extends Component
if (empty($this->dsn)) { if (empty($this->dsn)) {
throw new InvalidConfigException('Connection.dsn cannot be empty.'); throw new InvalidConfigException('Connection.dsn cannot be empty.');
} }
// TODO parse DSN $dsn = explode('/', $this->dsn);
$host = 'localhost'; $host = $dsn[2];
$port = 6379; if (strpos($host, ':')===false) {
try { $host .= ':6379';
}
$db = isset($dsn[3]) ? $dsn[3] : 0;
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
$this->_socket = stream_socket_client($host . ':' . $port); $this->_socket = @stream_socket_client($host, $errorNumber, $errorDescription);
// TODO auth if ($this->_socket) {
// TODO select database if ($this->password !== null) {
$this->initConnection(); $this->executeCommand('AUTH', array($this->password));
} }
catch (\PDOException $e) { $this->executeCommand('SELECT', array($db));
\Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); $this->initConnection();
$message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; } else {
throw new Exception($message, (int)$e->getCode(), $e->errorInfo); \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) { if ($this->_socket !== null) {
\Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); \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); stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
$this->_socket = null; $this->_socket = null;
$this->_transaction = null; $this->_transaction = null;
@ -278,9 +270,7 @@ class Connection extends Component
/** /**
* Initializes the DB connection. * Initializes the DB connection.
* This method is invoked right after the DB connection is established. * This method is invoked right after the DB connection is established.
* The default implementation turns on `PDO::ATTR_EMULATE_PREPARES` * The default implementation triggers an [[EVENT_AFTER_OPEN]] event.
* if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
* It then triggers an [[EVENT_AFTER_OPEN]] event.
*/ */
protected function initConnection() 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 string $name
* @param array $params * @param array $params
@ -333,21 +321,37 @@ class Connection extends Component
*/ */
public function __call($name, $params) public function __call($name, $params)
{ {
// TODO set active to true? $redisCommand = strtoupper(StringHelper::camel2words($name, false));
if (in_array($name, $this->redisCommands)) 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); array_unshift($params, $name);
$command = '*' . count($params) . "\r\n"; $command = '*' . count($params) . "\r\n";
foreach($params as $arg) { foreach($params as $arg) {
$command .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n"; $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); fwrite($this->_socket, $command);
return $this->parseResponse($command);
} return $this->parseResponse(implode(' ', $params));
else {
return parent::__call($name, $params);
}
} }
private function parseResponse($command) private function parseResponse($command)
@ -383,28 +387,7 @@ class Connection extends Component
} }
return $data; return $data;
default: default:
throw new Exception('Received illegal data from redis: ' . substr($line, 0, -2) . "\nRedis command was: " . $command); throw new Exception('Received illegal data from redis: ' . $line . "\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];
} }
return array($count, $time);
} }
} }

5
tests/unit/data/config.php

@ -7,4 +7,9 @@ return array(
'password' => '', 'password' => '',
'fixture' => __DIR__ . '/mysql.sql', 'fixture' => __DIR__ . '/mysql.sql',
), ),
'redis' => array(
'dsn' => 'redis://localhost:6379/0',
'password' => null,
// 'fixture' => __DIR__ . '/mysql.sql',
),
); );

45
tests/unit/framework/db/redis/ConnectionTest.php

@ -4,19 +4,44 @@ namespace yiiunit\framework\db\redis;
use yii\db\redis\Connection; use yii\db\redis\Connection;
class ConnectionTest extends RedisTestCase
{
/** /**
* * Empty DSN should throw exception
* * @expectedException \yii\base\InvalidConfigException
* @author Carsten Brandt <mail@cebe.cc>
*/ */
class ConnectionTest extends RedisTestCase public function testEmptyDSN()
{ {
public function testConstruct() $db = new Connection();
$db->open();
}
/**
* test connection to redis and selection of db
*/
public function testConnect()
{ {
$db = new Connection(); $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( return array(
array(123), array(123),
@ -24,18 +49,18 @@ class ConnectionTest extends RedisTestCase
array(0), array(0),
array('test'), array('test'),
array("test\r\ntest"), array("test\r\ntest"),
array(json_encode($this)), array(''),
); );
} }
/** /**
* @dataProvider storeGetData * @dataProvider keyValueData
*/ */
public function testStoreGet($data) public function testStoreGet($data)
{ {
$db = $this->getConnection(true); $db = $this->getConnection(true);
$db->SET('hi', $data); $db->set('hi', $data);
$this->assertEquals($data, $db->GET('hi')); $this->assertEquals($data, $db->get('hi'));
} }
} }

36
tests/unit/framework/db/redis/RedisTestCase.php

@ -1,39 +1,47 @@
<?php <?php
/**
*
*
* @author Carsten Brandt <mail@cebe.cc>
*/
namespace yiiunit\framework\db\redis; namespace yiiunit\framework\db\redis;
use yii\db\redis\Connection; use yii\db\redis\Connection;
use yiiunit\TestCase; use yiiunit\TestCase;
/**
* RedisTestCase is the base class for all redis related test cases
*/
class RedisTestCase extends TestCase class RedisTestCase extends TestCase
{ {
function __construct() protected function setUp()
{ {
// TODO check if a redis server is running $params = $this->getParam('redis');
//$this->markTestSkipped('No redis server running at port ...'); 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 * @param bool $reset whether to clean up the test database
* @return Connection * @return Connection
*/ */
function getConnection($reset = true) public function getConnection($reset = true)
{ {
$params = $this->getParam('redis'); $params = $this->getParam('redis');
$db = new \yii\db\redis\Connection; $db = new \yii\db\redis\Connection;
$db->dsn = $params['dsn']; $db->dsn = $params['dsn'];
$db->username = $params['username'];
$db->password = $params['password']; $db->password = $params['password'];
if ($reset) { if ($reset) {
// TODO implement $db->open();
/* $db->open(); $db->flushall();
$lines = explode(';', file_get_contents($params['fixture'])); /* $lines = explode(';', file_get_contents($params['fixture']));
foreach ($lines as $line) { foreach ($lines as $line) {
if (trim($line) !== '') { if (trim($line) !== '') {
$db->pdo->exec($line); $db->pdo->exec($line);

Loading…
Cancel
Save