diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..257221d23 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 59ea3d79b..b5aa1bef7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,10 @@ # Ignore all test and documentation for archive +/.github export-ignore +/.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.scrutinizer.yml export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore /tests export-ignore -/docs export-ignore \ No newline at end of file +/docs export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..b0924689b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +Contributing to Yii2 +==================== + +- [Report an issue](docs/internals/report-an-issue.md) +- [Translate documentation or messages](docs/internals/translation-workflow.md) +- [Give us feedback or start a design discussion](http://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/) +- [Contribute to the core code or fix bugs](docs/internals/git-workflow.md) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..52ec8692f --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +### What steps will reproduce the problem? + +### What's expected? + +### What do you get instead? + + +### Additional info + +| Q | A +| ---------------- | --- +| Yii vesion | +| PHP version | +| Operating system | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..968a845de --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +| Q | A +| ------------- | --- +| Is bugfix? | yes/no +| New feature? | yes/no +| Breaks BC? | yes/no +| Tests pass? | yes/no +| Fixed issues | comma-separated list of tickets # fixed by the PR, if any diff --git a/.travis.yml b/.travis.yml index def49ecea..150de8ef8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,21 +6,14 @@ php: - 5.6 - 7.0 - hhvm - - hhvm-nightly -services: - - redis-server - -# run build against hhvm but allow them to fail -# http://docs.travis-ci.com/user/build-configuration/#Rows-That-are-Allowed-To-Fail matrix: - fast_finish: true allow_failures: - - php: hhvm-nightly + - php: hhvm - php: 7.0 -# faster builds on new travis setup not using sudo -sudo: false +services: + - redis-server # cache vendor dirs cache: @@ -29,11 +22,14 @@ cache: install: - travis_retry composer self-update && composer --version - - travis_retry composer global require "fxp/composer-asset-plugin:~1.0.0" + - travis_retry composer global require "fxp/composer-asset-plugin:~1.1.1" - export PATH="$HOME/.composer/vendor/bin:$PATH" - travis_retry composer install --prefer-dist --no-interaction before_script: +# install phpredis extension. + - sh -c "git clone https://github.com/nicolasff/phpredis/ && cd phpredis && phpize && ./configure && make && sudo make install " > /dev/null + - echo "extension=redis.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` - | if [ $TRAVIS_PHP_VERSION = '5.6' ]; then PHPUNIT_FLAGS="--coverage-clover=coverage.clover" diff --git a/ActiveQuery.php b/ActiveQuery.php index c164a1429..24b86d96e 100644 --- a/ActiveQuery.php +++ b/ActiveQuery.php @@ -116,16 +116,11 @@ public function all($db = null) { // TODO add support for orderBy $data = $this->executeScript($db, 'All'); - $rows = []; - foreach ($data as $dataRow) { - $row = []; - $c = count($dataRow); - for ($i = 0; $i < $c;) { - $row[$dataRow[$i++]] = $dataRow[$i++]; - } - - $rows[] = $row; + if (empty($data)) { + return []; } + $rows = $this->parseList($data); + if (!empty($rows)) { $models = $this->createModels($rows); if (!empty($this->with)) { @@ -158,11 +153,8 @@ public function one($db = null) if (empty($data)) { return null; } - $row = []; - $c = count($data); - for ($i = 0; $i < $c;) { - $row[$data[$i++]] = $data[$i++]; - } + $row = $this->parseRow($data); + if ($this->asArray) { $model = $row; } else { @@ -307,7 +299,7 @@ public function scalar($attribute, $db = null) * @param string $type the type of the script to generate * @param string $columnName * @throws NotSupportedException - * @return array|bool|null|string + * @return array|boolean|null|string */ protected function executeScript($db, $type, $columnName = null) { @@ -347,7 +339,10 @@ protected function executeScript($db, $type, $columnName = null) } // convert inCondition for one key - if (is_array($this->where) && isset($this->where[0]) && $this->where[0] == 'in' && count($this->where[1]) == 1) { + if (is_array($this->where) && isset($this->where[0]) && $this->where[0] == 'in' && count( + $this->where[1] + ) == 1 + ) { $this->where = [current($this->where[1]) => $this->where[2]]; } @@ -359,7 +354,7 @@ protected function executeScript($db, $type, $columnName = null) $method = 'build' . $type; $script = $db->getLuaScriptBuilder()->$method($this, $columnName); - return $db->executeCommand('EVAL', [$script, 0]); + return $db->executeCommand('EVAL', [$script]); } /** @@ -368,14 +363,14 @@ protected function executeScript($db, $type, $columnName = null) * If this parameter is not given, the `db` application component will be used. * @param string $type the type of the script to generate * @param string $columnName - * @return array|bool|null|string + * @return array|boolean|null|string * @throws \yii\base\InvalidParamException * @throws \yii\base\NotSupportedException */ private function findByPk($db, $type, $columnName = null) { if (count($this->where) == 1) { - $pks = (array) reset($this->where); + $pks = (array)reset($this->where); } else { foreach ($this->where as $values) { if (is_array($values)) { @@ -488,4 +483,29 @@ private function findByPk($db, $type, $columnName = null) } throw new InvalidParamException('Unknown fetch type: ' . $type); } + + private function parseList($data) + { + $result = []; + foreach ($data as $list) { + $result[] = $this->parseRow($list); + } + + return $result; + } + + private function parseRow($row) + { + if (!isset($row[0])) { + return $row; + } + + $result = []; + $c = count($row); + + for ($i = 0; $i < $c;) { + $result[$row[$i++]] = $row[$i++]; + } + return $result; + } } diff --git a/ActiveRecord.php b/ActiveRecord.php index 07ee22c91..2cfe7ccaf 100644 --- a/ActiveRecord.php +++ b/ActiveRecord.php @@ -46,7 +46,7 @@ class ActiveRecord extends BaseActiveRecord */ public static function getDb() { - return \Yii::$app->get('redis'); + return Yii::$app->get('redis'); } /** @@ -126,20 +126,19 @@ public function insert($runValidation = true, $attributes = null) $key = static::keyPrefix() . ':a:' . static::buildKey($pk); // save attributes - $setArgs = [$key]; + $setArgs = []; foreach ($values as $attribute => $value) { // only insert attributes that are not null if ($value !== null) { if (is_bool($value)) { $value = (int) $value; } - $setArgs[] = $attribute; - $setArgs[] = $value; + $setArgs[$attribute] = $value; } } if (count($setArgs) > 1) { - $db->executeCommand('HMSET', $setArgs); + $db->executeCommand('HMSET', [$key, $setArgs]); } $changedAttributes = array_fill_keys(array_keys($values), null); @@ -174,8 +173,6 @@ public static function updateAll($attributes, $condition = null) $pk = static::buildKey($pk); $key = static::keyPrefix() . ':a:' . $pk; // save attributes - $delArgs = [$key]; - $setArgs = [$key]; foreach ($attributes as $attribute => $value) { if (isset($newPk[$attribute])) { $newPk[$attribute] = $value; @@ -184,10 +181,9 @@ public static function updateAll($attributes, $condition = null) if (is_bool($value)) { $value = (int) $value; } - $setArgs[] = $attribute; - $setArgs[] = $value; + $setArgs[$attribute] = $value; } else { - $delArgs[] = $attribute; + $delArgs = $attribute; } } $newPk = static::buildKey($newPk); @@ -195,22 +191,22 @@ public static function updateAll($attributes, $condition = null) // rename index if pk changed if ($newPk != $pk) { $db->executeCommand('MULTI'); - if (count($setArgs) > 1) { - $db->executeCommand('HMSET', $setArgs); + if (!empty($setArgs)) { + $db->executeCommand('HMSET', [$key, $setArgs]); } - if (count($delArgs) > 1) { - $db->executeCommand('HDEL', $delArgs); + if (!empty($delArgs)) { + $db->executeCommand('HDEL', [$key, $delArgs]); } $db->executeCommand('HSET', [static::keyPrefix(), $newPk, 0]); $db->executeCommand('HDEL', [static::keyPrefix(), $pk]); $db->executeCommand('RENAME', [$key, $newKey]); $db->executeCommand('EXEC'); } else { - if (count($setArgs) > 1) { - $db->executeCommand('HMSET', $setArgs); + if (!empty($setArgs)) { + $db->executeCommand('HMSET', [$key, $setArgs]); } - if (count($delArgs) > 1) { - $db->executeCommand('HDEL', $delArgs); + if (!empty($delArgs)) { + $db->executeCommand('HDEL', [$key, $delArgs]); } } $n++; diff --git a/CHANGELOG.md b/CHANGELOG.md index a69acb95f..063467091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,21 @@ Yii Framework 2 redis extension Change Log ========================================== -2.0.5 under development +2.0.6 under development ----------------------- -- Chg #14: Added missing `BLPOP` to `$redisCommands` (samdark) +- no changes in this release. + + +2.0.5 March 17, 2016 +-------------------- + +- Bug #22: Fixed string escaping issue in LuaScriptBuilder (vistart) +- Bug #37: Fixed detection of open socket (mirocow) +- Bug #46: Fixed bug to execute session_regenerate_id in PHP 7.0 (githubjeka) +- Enh #31: Added `Connection::$socketClientFlags` property for connection flags to be passed to `stream_socket_client()` (hugh-lee) +- Chg #14: Added missing `BLPOP` command to `$redisCommands` (samdark) +- Chg #61: Added missing `GEO*` commands to `$redisCommands` (leadermt) 2.0.4 May 10, 2015 diff --git a/Cache.php b/Cache.php index 995a0d7d8..11f13f2ca 100644 --- a/Cache.php +++ b/Cache.php @@ -8,7 +8,7 @@ namespace yii\redis; use Yii; -use yii\base\InvalidConfigException; +use yii\di\Instance; /** * Redis Cache implements a cache application component based on [redis](http://redis.io/) key-value store. @@ -76,17 +76,7 @@ class Cache extends \yii\caching\Cache public function init() { parent::init(); - if (is_string($this->redis)) { - $this->redis = Yii::$app->get($this->redis); - } elseif (is_array($this->redis)) { - if (!isset($this->redis['class'])) { - $this->redis['class'] = Connection::className(); - } - $this->redis = Yii::createObject($this->redis); - } - if (!$this->redis instanceof Connection) { - throw new InvalidConfigException("Cache::redis must be either a Redis connection instance or the application component ID of a Redis connection."); - } + $this->redis = Instance::ensure($this->redis, Connection::className()); } /** @@ -117,7 +107,7 @@ protected function getValue($key) */ protected function getValues($keys) { - $response = $this->redis->executeCommand('MGET', $keys); + $response = $this->redis->executeCommand('MGET', [$keys]); $result = []; $i = 0; foreach ($keys as $key) { @@ -133,11 +123,11 @@ protected function getValues($keys) protected function setValue($key, $value, $expire) { if ($expire == 0) { - return (bool) $this->redis->executeCommand('SET', [$key, $value]); + return (bool) $this->redis->executeCommand('SETNX', [$key, $value]); } else { $expire = (int) ($expire * 1000); - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire]); + return (bool) $this->redis->executeCommand('PSETEX', [$key, $expire, $value]); } } @@ -146,19 +136,13 @@ protected function setValue($key, $value, $expire) */ protected function setValues($data, $expire) { - $args = []; - foreach ($data as $key => $value) { - $args[] = $key; - $args[] = $value; - } - $failedKeys = []; if ($expire == 0) { - $this->redis->executeCommand('MSET', $args); + $this->redis->executeCommand('MSET', [$data]); } else { $expire = (int) ($expire * 1000); $this->redis->executeCommand('MULTI'); - $this->redis->executeCommand('MSET', $args); + $this->redis->executeCommand('MSET', [$data]); $index = []; foreach ($data as $key => $value) { $this->redis->executeCommand('PEXPIRE', [$key, $expire]); @@ -182,11 +166,11 @@ protected function setValues($data, $expire) protected function addValue($key, $value, $expire) { if ($expire == 0) { - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'NX']); + return (bool) $this->redis->executeCommand('SETNX', [$key, $value]); } else { $expire = (int) ($expire * 1000); - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']); + return (bool) $this->redis->executeCommand('PSETEX', [$key, $expire, $value]); } } diff --git a/Connection.php b/Connection.php index 34d4482c4..88343abf7 100644 --- a/Connection.php +++ b/Connection.php @@ -69,9 +69,9 @@ class Connection extends Component */ public $database = 0; /** - * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + * @var float timeout to use for connection to redis. */ - public $connectionTimeout = null; + public $connectionTimeout = 10; /** * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. */ @@ -220,12 +220,18 @@ class Connection extends Component 'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low 'ZSCORE', // key member Get the score associated with the given member in a sorted set 'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key + 'GEOADD', // key longitude latitude member [longitude latitude member ...] Add point + 'GEODIST', // key member1 member2 [unit] Return the distance between two members + 'GEOHASH', // key member [member ...] Return valid Geohash strings + 'GEOPOS', // key member [member ...] Return the positions (longitude,latitude) + 'GEORADIUS', // key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] Return the members + 'GEORADIUSBYMEMBER', // key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] ]; /** - * @var resource redis socket connection + * @var \Redis resource redis socket connection */ - private $_socket; + private $_client = false; /** @@ -235,7 +241,6 @@ class Connection extends Component public function __sleep() { $this->close(); - return array_keys(get_object_vars($this)); } @@ -245,7 +250,7 @@ public function __sleep() */ public function getIsActive() { - return $this->_socket !== null; + return $this->_client !== false; } /** @@ -255,30 +260,37 @@ public function getIsActive() */ public function open() { - if ($this->_socket !== null) { + if ($this->_client !== false) { return; } $connection = ($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database; \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__); - $this->_socket = @stream_socket_client( - $this->unixSocket ? 'unix://' . $this->unixSocket : 'tcp://' . $this->hostname . ':' . $this->port, - $errorNumber, - $errorDescription, - $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") - ); - if ($this->_socket) { - if ($this->dataTimeout !== null) { - stream_set_timeout($this->_socket, $timeout = (int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); - } + $this->_client = new \Redis; + if ($this->unixSocket) { + $connected = $this->_client->connect($this->unixSocket); + } else { + $connected = $this->_client->connect($this->hostname, $this->port, $this->connectionTimeout, null, 1000); + } + + if ($connected) { if ($this->password !== null) { $this->executeCommand('AUTH', [$this->password]); } $this->executeCommand('SELECT', [$this->database]); $this->initConnection(); } else { - \Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__); - $message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.'; - throw new Exception($message, $errorDescription, (int) $errorNumber); + $message = [ + 'Failed to open redis DB connection', + ' (' . $connection . '): ' . $this->_client->getLastError(), + ]; + + \Yii::error(implode('', $message), __CLASS__); + + if (!YII_DEBUG) { + $message[1] .= '.'; + } + + throw new Exception(implode('', $message), $this->_client->getLastError()); } } @@ -288,12 +300,11 @@ public function open() */ public function close() { - if ($this->_socket !== null) { + if ($this->_client !== false) { $connection = ($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database; \Yii::trace('Closing DB connection: ' . $connection, __METHOD__); - $this->executeCommand('QUIT'); - stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); - $this->_socket = null; + $this->_client->close(); + $this->_client = null; } } @@ -351,7 +362,7 @@ public function __call($name, $params) * * @param string $name the name of the command * @param array $params list of parameters for the command - * @return array|bool|null|string Dependent on the executed command this method + * @return array|boolean|null|string Dependent on the executed command this method * will return different data types: * * - `true` for commands that return "status reply" with the message `'OK'` or `'PONG'`. @@ -368,79 +379,22 @@ public function __call($name, $params) public function executeCommand($name, $params = []) { $this->open(); + $token = 'Command: ' . $name . PHP_EOL . 'Params: ' . var_export($params, true); - array_unshift($params, $name); - $command = '*' . count($params) . "\r\n"; - foreach ($params as $arg) { - $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; - } - - $token = implode("\n", $params); \Yii::beginProfile($token, 'yii\db\Command::query'); - fwrite($this->_socket, $command); - - $response = $this->parseResponse(implode(' ', $params)); + $response = call_user_func_array([$this->_client, $name], $params); \Yii::endProfile($token, 'yii\db\Command::query'); + return $response; } - /** - * @param string $command - * @return mixed - * @throws Exception on error - */ - public function parseResponse($command) + public function getError() { - if (($line = $this->read()) === false) { - throw new Exception("Failed to read from socket.\nRedis command was: " . $command); - } - $type = $line[0]; - $line = mb_substr($line, 1, -2, '8bit'); - switch ($type) { - case '+': // Status reply - if ($line === 'OK' || $line === 'PONG') { - return true; - } else { - return $line; - } - case '-': // Error reply - throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command); - case ':': // Integer reply - // no cast to int as it is in the range of a signed 64 bit integer - return $line; - case '$': // Bulk replies - if ($line == '-1') { - return null; - } - $length = $line + 2; - $data = ''; - while ($length > 0) { - if (($block = $this->read($length)) === false) { - throw new Exception("Failed to read from socket.\nRedis command was: " . $command); - } - $data .= $block; - $length -= mb_strlen($block, '8bit'); - } - - return mb_substr($data, 0, -2, '8bit'); - case '*': // Multi-bulk replies - $count = (int) $line; - $data = []; - for ($i = 0; $i < $count; $i++) { - $data[] = $this->parseResponse($command); - } - - return $data; - default: - throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); - } + return $this->_client->getLastError(); } - private function read($length = null) { - if($length) { - return fread($this->_socket, $length); - } - - return fgets($this->_socket); + public function getClient() + { + return $this->_client; } } diff --git a/LuaScriptBuilder.php b/LuaScriptBuilder.php index b038e08ee..e032dd273 100644 --- a/LuaScriptBuilder.php +++ b/LuaScriptBuilder.php @@ -215,7 +215,7 @@ private function quoteValue($str) return $str; } - return "'" . addcslashes(str_replace("'", "\\'", $str), "\000\n\r\\\032") . "'"; + return "'" . addcslashes($str, "\000\n\r\\\032\047") . "'"; } /** diff --git a/Makefile b/Makefile index 13a96ae36..b6dd0e982 100644 --- a/Makefile +++ b/Makefile @@ -5,14 +5,16 @@ REDIS_VERSION=3.0.0 PHP_VERSION=php-5.6.8 YII_VERSION=dev-master +# ensure all the configuration variables above are in environment of the shell commands below +export help: @echo "make test - run phpunit tests using a docker environment" @echo "make clean - stop docker and remove container" test: docker adjust-config - composer require "yiisoft/yii2:${YII_VERSION}" --prefer-dist - composer install --prefer-dist + #composer require "yiisoft/yii2:${YII_VERSION}" --prefer-dist + #composer install --prefer-dist docker run --rm=true -v $(shell pwd):/opt/test --link $(shell cat tests/dockerids/redis):redis yiitest/php:${PHP_VERSION} phpunit --verbose --color adjust-config: diff --git a/README.md b/README.md index 506396b4e..b0fcfac0a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ yii2-redis module with various fixes and speedups * [fix populate null attributes](https://github.com/E96/yii2-redis/commit/02c879b865937b39f47fb0664329310dcaf3ff94) * [fix integer values in buildKey](https://github.com/E96/yii2-redis/commit/da6ed85ed15bd33b4137083403c027a15b9fc03d) * [Make Connection::parseResponse public](https://github.com/E96/yii2-redis/commit/f5ce8325303cda6bd1bc1ecb62d310a5b01661e4) +* [change connection from socket to phpredis](https://github.com/E96/yii2-redis/commit/d28911a005a02cd3b01940bb8e959813599b287a) Migration guide =============== diff --git a/Session.php b/Session.php index 70a74b7dc..9617a6168 100644 --- a/Session.php +++ b/Session.php @@ -116,7 +116,7 @@ public function readSession($id) { $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); - return $data === false ? '' : $data; + return $data === false || $data === null ? '' : $data; } /** @@ -128,7 +128,7 @@ public function readSession($id) */ public function writeSession($id, $data) { - return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); + return (bool)$this->redis->executeCommand('SETEX', [$this->calculateKey($id), $this->getTimeout(), $data]); } /** diff --git a/docs/guide-ja/usage-commands.md b/docs/guide-ja/usage-commands.md index 3fa8b40e4..9fd3bedd2 100644 --- a/docs/guide-ja/usage-commands.md +++ b/docs/guide-ja/usage-commands.md @@ -18,7 +18,7 @@ $result = $redis->executeCommand('hmset', ['test_collection', 'key1', 'val1', 'k サポートされているコマンドのそれぞれに対してショートカットが利用できますので、上記の代りに次のようにすることも出来ます。 ```php -$result = $redis->hmset(['test_collection', 'key1', 'val1', 'key2', 'val2']); +$result = $redis->hmset('test_collection', 'key1', 'val1', 'key2', 'val2'); ``` 利用できるコマンドとそのパラメータについては、[http://redis.io/commands](http://redis.io/commands) のリストを参照してください。 diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md new file mode 100644 index 000000000..e317ae368 --- /dev/null +++ b/docs/guide-zh-CN/README.md @@ -0,0 +1,24 @@ +Yii 2 Redis 缓存,会话和活动记录 +=============================================== + +此扩展为 Yii2 framework 提供了 [redis](http://redis.io/) 键-值存储的支持。 +包含了一个 `Cache` 和 `Session` 存储句柄并且实现了 `ActiveRecord` 模式,允许 +你存储活动记录到 redis 中。 + + +入门指南 +--------------- + +* [安装](installation.md) + +用法 +----- + +* [活动记录的使用](usage-ar.md) +* [直接使用命令](usage-commands.md) + +附加主题 +----------------- + +* [缓存组件的使用](topics-cache.md) +* [会话组件的使用](topics-session.md) diff --git a/docs/guide-zh-CN/installation.md b/docs/guide-zh-CN/installation.md new file mode 100644 index 000000000..449e69722 --- /dev/null +++ b/docs/guide-zh-CN/installation.md @@ -0,0 +1,42 @@ +安装 +============ + +## 需求 + +redis 2.6.12 版本是所有部件正常工作所必需的。 + +## 获取 Composer 安装包 + +安装此扩展的首选方式是通过 [composer](http://getcomposer.org/download/)。 + +可以运行 + +``` +php composer.phar require --prefer-dist yiisoft/yii2-redis +``` + +或者添加 + +```json +"yiisoft/yii2-redis": "~2.0.0" +``` + +在 `composer.json` 文件中的必要部分。 + +## 配置应用程序 + +使用此扩展时,需要在你的应用程序配置中配置 Connection 类: + +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\Connection', + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + ], + ] +]; +``` diff --git a/docs/guide-zh-CN/topics-cache.md b/docs/guide-zh-CN/topics-cache.md new file mode 100644 index 000000000..a1d6f5331 --- /dev/null +++ b/docs/guide-zh-CN/topics-cache.md @@ -0,0 +1,37 @@ +缓存组件的使用 +========================= + +为了使用 `Cache` 组件,如 [安装](installation.md) 章节中所描述的,除了配置连接, +你也需要配置 `yii\redis\Cache` 中的 `cache` 组件: + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +如果你只使用 redis 缓存(即,不使用它的活动记录或者会话),您还可以配置缓存组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + ], + ], + ] +]; +``` diff --git a/docs/guide-zh-CN/topics-session.md b/docs/guide-zh-CN/topics-session.md new file mode 100644 index 000000000..80ef0a987 --- /dev/null +++ b/docs/guide-zh-CN/topics-session.md @@ -0,0 +1,37 @@ +会话组件的使用 +=========================== + +为了使用 `Session` 组件,如 [安装](installation.md) 章节中所描述的,除了配置连接, +你也需要配置 `yii\redis\Session` 中的 `session` 组件: + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +如果你只使用 redis 会话(即,不使用它的活动记录或者缓存),您还可以配置会话组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + ], + ], + ] +]; +``` diff --git a/docs/guide-zh-CN/usage-ar.md b/docs/guide-zh-CN/usage-ar.md new file mode 100644 index 000000000..d662bac8f --- /dev/null +++ b/docs/guide-zh-CN/usage-ar.md @@ -0,0 +1,64 @@ +活动记录的使用 +====================== + +对于如何使用 yii 的活动记录一般信息请参阅 [指南](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md)。 + +定义一个 redis 活动记录类,你的记录类需要继承自 [[yii\redis\ActiveRecord]] 并且 +至少实现 `attributes()` 方法来定义记录的属性。 +一个没有指定默认值,默认为 `id` 的主键可以通过 [[yii\redis\ActiveRecord::primaryKey()]] 定义。 +主键是属性中必要的一部分,所以请确保你有一个 `id` 属性定义的, +如果你没有指定自己的主键。 + +以下是一个 `Customer` 的实例模型: + +```php +class Customer extends \yii\redis\ActiveRecord +{ + /** + * @return array 此记录的属性列表 + */ + public function attributes() + { + return ['id', 'name', 'address', 'registration_date']; + } + + /** + * @return ActiveQuery 定义一个关联到 Order 的记录(可以在其它数据库中,例如 elasticsearch 或者 sql) + */ + public function getOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id']); + } + + /** + * 定义一个修改 `$query` 的范围返回有效(status = 1)的客户。 + */ + public static function active($query) + { + $query->andWhere(['status' => 1]); + } +} +``` + +redis 活动记录的一般用法和数据库活动记录非常相似,正如 +[指南](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md) 中所描述的。 +它支持相同的界面和功能,除了以下限制: + +- redis 不支持 SQL 查询的 API 仅限于以下方法: + `where()`,`limit()`,`offset()`,`orderBy()` 和 `indexBy()`。 + (orderBy() 尚未实现:[#1305](https://github.com/yiisoft/yii2/issues/1305)) +- `via`-关系不能通过在 redis 中没有的表定义。你只能通过其他记录来定义关系。 + +另外,也可以定义从 redis 的活动记录关系到正常的活动记录类,反之亦然。 + +使用实例: + +```php +$customer = new Customer(); +$customer->attributes = ['name' => 'test']; +$customer->save(); +echo $customer->id; // 如果没有明确设置 id 会自动递增 + +$customer = Customer::find()->where(['name' => 'test'])->one(); // 通过 query 查找 +$customer = Customer::find()->active()->all(); // 通过 query 查找全部(使用 `active` 范围) +``` diff --git a/docs/guide-zh-CN/usage-commands.md b/docs/guide-zh-CN/usage-commands.md new file mode 100644 index 000000000..ffab5814f --- /dev/null +++ b/docs/guide-zh-CN/usage-commands.md @@ -0,0 +1,23 @@ +直接使用命令行 +======================= + +Redis 有很多可以直接从连接中使用的有用的命令。在配置应用程序后, +如 [安装](installation.md) 所示,连接可以像下面这样获取: + +```php +$redis = Yii::$app->redis; +``` + +完成之后可以执行如下命令。最通用的方法是使用 `executeCommand` 方法: + +```php +$result = $redis->executeCommand('hmset', ['test_collection', 'key1', 'val1', 'key2', 'val2']); +``` + +每个命令都有相应的快捷方式支持,所以可以像下面这样代替以上的命令: + +```php +$result = $redis->hmset(['test_collection', 'key1', 'val1', 'key2', 'val2']); +``` + +可用命令列表和他们的参数可参阅 [http://redis.io/commands](http://redis.io/commands)。 \ No newline at end of file diff --git a/docs/guide/usage-commands.md b/docs/guide/usage-commands.md index 2b2f07a7e..5af2d384f 100644 --- a/docs/guide/usage-commands.md +++ b/docs/guide/usage-commands.md @@ -17,7 +17,7 @@ $result = $redis->executeCommand('hmset', ['test_collection', 'key1', 'val1', 'k There are shortcuts available for each command supported so insted of the above it can be used as follows: ```php -$result = $redis->hmset(['test_collection', 'key1', 'val1', 'key2', 'val2']); +$result = $redis->hmset('test_collection', 'key1', 'val1', 'key2', 'val2'); ``` -For a list of available commands and their parameters see [http://redis.io/commands](http://redis.io/commands). \ No newline at end of file +For a list of available commands and their parameters see [http://redis.io/commands](http://redis.io/commands). diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 2aff05ca6..4b697b7f2 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -442,4 +442,25 @@ public function testAttributeChanged() $model->items = [1, 2, 3]; $this->assertTrue($model->isAttributeChanged('items')); } + + public function testEscapeData() + { + $customer = new Customer(); + $customer->email = "the People's Republic of China"; + $customer->save(false); + + $c = Customer::findOne(['email' => "the People's Republic of China"]); + $this->assertSame("the People's Republic of China", $c->email); + } + + public function testFindEmptyWith() + { + Order::getDb()->flushdb(); + $orders = Order::find() + ->where(['total' => 100000]) + ->orWhere(['total' => 1]) + ->with('customer') + ->all(); + $this->assertEquals([], $orders); + } } diff --git a/tests/RedisCacheTest.php b/tests/RedisCacheTest.php index 631fb44f5..ea745f24b 100644 --- a/tests/RedisCacheTest.php +++ b/tests/RedisCacheTest.php @@ -20,7 +20,7 @@ class RedisCacheTest extends CacheTestCase */ protected function getCacheInstance() { - $databases = self::getParam('databases'); + $databases = TestCase::getParam('databases'); $params = isset($databases['redis']) ? $databases['redis'] : null; if ($params === null) { $this->markTestSkipped('No redis server connection configured.'); diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index e54fddce0..dedda956c 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -15,7 +15,7 @@ public function testConnect() $db = $this->getConnection(false); $database = $db->database; $db->open(); - $this->assertTrue($db->ping()); + $this->assertEquals('+PONG', $db->ping()); $db->set('YIITESTKEY', 'YIITESTVALUE'); $db->close(); @@ -28,7 +28,7 @@ public function testConnect() $db = $this->getConnection(false); $db->database = 1; $db->open(); - $this->assertNull($db->get('YIITESTKEY')); + $this->assertFalse($db->get('YIITESTKEY')); $db->close(); } @@ -62,7 +62,7 @@ public function testReturnType() { $redis = $this->getConnection(); $redis->executeCommand('SET',['key1','val1']); - $redis->executeCommand('HMSET',['hash1','hk3','hv3','hk4','hv4']); + $redis->executeCommand('HMSET', ['hash1', ['hk3' => 'hv3', 'hk4' => 'hv4']]); $redis->executeCommand('RPUSH',['newlist2','tgtgt','tgtt','44',11]); $redis->executeCommand('SADD',['newset2','segtggttval','sv1','sv2','sv3']); $redis->executeCommand('ZADD',['newz2',2,'ss',3,'pfpf']); @@ -70,11 +70,11 @@ public function testReturnType() sort($allKeys); $this->assertEquals(['hash1', 'key1', 'newlist2', 'newset2', 'newz2'], $allKeys); $expected = [ - 'hash1' => 'hash', - 'key1' => 'string', - 'newlist2' => 'list', - 'newset2' => 'set', - 'newz2' => 'zset', + 'hash1' => \Redis::REDIS_HASH, + 'key1' => \Redis::REDIS_STRING, + 'newlist2' => \Redis::REDIS_LIST, + 'newset2' => \Redis::REDIS_SET, + 'newz2' => \Redis::REDIS_ZSET, ]; foreach($allKeys as $key) { $this->assertEquals($expected[$key], $redis->executeCommand('TYPE',[$key])); diff --git a/tests/data/ar/Customer.php b/tests/data/ar/Customer.php index 1a0678c40..775be5d87 100644 --- a/tests/data/ar/Customer.php +++ b/tests/data/ar/Customer.php @@ -35,6 +35,11 @@ public function getExpensiveOrders() return $this->hasMany(Order::className(), ['customer_id' => 'id'])->andWhere("tonumber(redis.call('HGET','order' .. ':a:' .. pk, 'total')) > 50"); } + public function getOrdersWithItems() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->with('orderItems'); + } + /** * @return \yii\redis\ActiveQuery */