mirror of
https://github.com/yiisoft/yii2.git
synced 2025-11-03 22:32:40 +08:00
Finished Redis Connection class
This commit is contained in:
@ -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 = '';
|
public $password;
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
/**
|
|
||||||
* @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';
|
||||||
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
|
|
||||||
$this->_socket = stream_socket_client($host . ':' . $port);
|
|
||||||
// TODO auth
|
|
||||||
// TODO select database
|
|
||||||
$this->initConnection();
|
|
||||||
}
|
}
|
||||||
catch (\PDOException $e) {
|
$db = isset($dsn[3]) ? $dsn[3] : 0;
|
||||||
\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.';
|
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
|
||||||
throw new Exception($message, (int)$e->getCode(), $e->errorInfo);
|
$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) {
|
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,23 +321,39 @@ 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);
|
||||||
array_unshift($params, $name);
|
} else {
|
||||||
$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 {
|
|
||||||
return parent::__call($name, $params);
|
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)
|
private function parseResponse($command)
|
||||||
{
|
{
|
||||||
if(($line = fgets($this->_socket))===false) {
|
if(($line = fgets($this->_socket))===false) {
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,19 +4,44 @@ namespace yiiunit\framework\db\redis;
|
|||||||
|
|
||||||
use yii\db\redis\Connection;
|
use yii\db\redis\Connection;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @author Carsten Brandt <mail@cebe.cc>
|
|
||||||
*/
|
|
||||||
class ConnectionTest extends RedisTestCase
|
class ConnectionTest extends RedisTestCase
|
||||||
{
|
{
|
||||||
public function testConstruct()
|
/**
|
||||||
|
* Empty DSN should throw exception
|
||||||
|
* @expectedException \yii\base\InvalidConfigException
|
||||||
|
*/
|
||||||
|
public function testEmptyDSN()
|
||||||
{
|
{
|
||||||
$db = new Connection();
|
$db = new Connection();
|
||||||
|
$db->open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeGetData()
|
/**
|
||||||
|
* 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 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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user