From 8ac2b7364d6435683505fd6b24bb36ab4ec1438a Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 25 Nov 2013 02:23:51 +0100 Subject: [PATCH] moved redis to extensions --- composer.json | 2 + extensions/redis/ActiveQuery.php | 382 +++++++++++++++++ extensions/redis/ActiveRecord.php | 322 ++++++++++++++ extensions/redis/ActiveRelation.php | 67 +++ extensions/redis/Cache.php | 184 ++++++++ extensions/redis/Connection.php | 406 ++++++++++++++++++ extensions/redis/LICENSE.md | 32 ++ extensions/redis/LuaScriptBuilder.php | 365 ++++++++++++++++ extensions/redis/README.md | 91 ++++ extensions/redis/composer.json | 27 ++ extensions/sphinx/composer.json | 2 +- framework/yii/caching/RedisCache.php | 218 ---------- framework/yii/classes.php | 3 - framework/yii/redis/ActiveQuery.php | 382 ----------------- framework/yii/redis/ActiveRecord.php | 322 -------------- framework/yii/redis/ActiveRelation.php | 67 --- framework/yii/redis/Connection.php | 405 ------------------ framework/yii/redis/LuaScriptBuilder.php | 365 ---------------- tests/unit/data/config.php | 4 +- tests/unit/extensions/redis/ActiveRecordTest.php | 465 ++++++++++++++++++++ tests/unit/extensions/redis/RedisCacheTest.php | 101 +++++ .../unit/extensions/redis/RedisConnectionTest.php | 58 +++ tests/unit/extensions/redis/RedisTestCase.php | 48 +++ tests/unit/framework/caching/RedisCacheTest.php | 95 ----- tests/unit/framework/redis/ActiveRecordTest.php | 466 --------------------- tests/unit/framework/redis/RedisConnectionTest.php | 69 --- tests/unit/framework/redis/RedisTestCase.php | 51 --- 27 files changed, 2554 insertions(+), 2445 deletions(-) create mode 100644 extensions/redis/ActiveQuery.php create mode 100644 extensions/redis/ActiveRecord.php create mode 100644 extensions/redis/ActiveRelation.php create mode 100644 extensions/redis/Cache.php create mode 100644 extensions/redis/Connection.php create mode 100644 extensions/redis/LICENSE.md create mode 100644 extensions/redis/LuaScriptBuilder.php create mode 100644 extensions/redis/README.md create mode 100644 extensions/redis/composer.json delete mode 100644 framework/yii/caching/RedisCache.php delete mode 100644 framework/yii/redis/ActiveQuery.php delete mode 100644 framework/yii/redis/ActiveRecord.php delete mode 100644 framework/yii/redis/ActiveRelation.php delete mode 100644 framework/yii/redis/Connection.php delete mode 100644 framework/yii/redis/LuaScriptBuilder.php create mode 100644 tests/unit/extensions/redis/ActiveRecordTest.php create mode 100644 tests/unit/extensions/redis/RedisCacheTest.php create mode 100644 tests/unit/extensions/redis/RedisConnectionTest.php create mode 100644 tests/unit/extensions/redis/RedisTestCase.php delete mode 100644 tests/unit/framework/caching/RedisCacheTest.php delete mode 100644 tests/unit/framework/redis/ActiveRecordTest.php delete mode 100644 tests/unit/framework/redis/RedisConnectionTest.php delete mode 100644 tests/unit/framework/redis/RedisTestCase.php diff --git a/composer.json b/composer.json index ee0766a..4cc1743 100644 --- a/composer.json +++ b/composer.json @@ -69,6 +69,7 @@ "yiisoft/yii2-debug": "self.version", "yiisoft/yii2-gii": "self.version", "yiisoft/yii2-jui": "self.version", + "yiisoft/yii2-redis": "self.version", "yiisoft/yii2-smarty": "self.version", "yiisoft/yii2-swiftmailer": "self.version", "yiisoft/yii2-sphinx": "self.version", @@ -95,6 +96,7 @@ "yii\\debug\\": "extensions/debug/", "yii\\gii\\": "extensions/gii/", "yii\\jui\\": "extensions/jui/", + "yii\\redis\\": "extensions/redis/", "yii\\smarty\\": "extensions/smarty/", "yii\\swiftmailer\\": "extensions/swiftmailer/", "yii\\sphinx\\": "extensions/sphinx/", diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php new file mode 100644 index 0000000..2174901 --- /dev/null +++ b/extensions/redis/ActiveQuery.php @@ -0,0 +1,382 @@ +with('orders')->asArray()->all(); + * ~~~ + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface +{ + use QueryTrait; + use ActiveQueryTrait; + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. + */ + 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($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + return $models; + } else { + return []; + } + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + // TODO add support for orderBy + $data = $this->executeScript($db, 'One'); + if (empty($data)) { + return null; + } + $row = []; + $c = count($data); + for($i = 0; $i < $c; ) { + $row[$data[$i++]] = $data[$i++]; + } + if ($this->asArray) { + $model = $row; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::create($row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + return $model; + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + if ($this->offset === null && $this->limit === null && $this->where === null) { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + return $db->executeCommand('LLEN', [$modelClass::tableName()]); + } else { + return $this->executeScript($db, 'Count'); + } + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + return $this->one($db) !== null; + } + + /** + * Executes the query and returns the first column of the result. + * @param string $column name of the column to select + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($column, $db = null) + { + // TODO add support for orderBy + return $this->executeScript($db, 'Column', $column); + } + + /** + * Returns the number of records. + * @param string $column the column to sum up + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function sum($column, $db = null) + { + return $this->executeScript($db, 'Sum', $column); + } + + /** + * Returns the average of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($column, $db = null) + { + return $this->executeScript($db, 'Average', $column); + } + + /** + * Returns the minimum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($column, $db = null) + { + return $this->executeScript($db, 'Min', $column); + } + + /** + * Returns the maximum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($column, $db = null) + { + return $this->executeScript($db, 'Max', $column); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the specified attribute in the first record of the query results. + * @param string $attribute name of the attribute to select + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return string the value of the specified attribute in the first record of the query result. + * Null is returned if the query result is empty. + */ + public function scalar($attribute, $db = null) + { + $record = $this->one($db); + if ($record !== null) { + return $record->$attribute; + } else { + return null; + } + } + + + /** + * Executes a script created by [[LuaScriptBuilder]] + * @param Connection $db the database connection used to execute the query. + * 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 + */ + protected function executeScript($db, $type, $columnName = null) + { + if (!empty($this->orderBy)) { + throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); + } + + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + + if ($db === null) { + $db = $modelClass::getDb(); + } + + // find by primary key if possible. This is much faster than scanning all records + if (is_array($this->where) && !isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where))) { + return $this->findByPk($db, $type, $columnName); + } + + $method = 'build' . $type; + $script = $db->getLuaScriptBuilder()->$method($this, $columnName); + return $db->executeCommand('EVAL', [$script, 0]); + } + + /** + * Fetch by pk if possible as this is much faster + * @param Connection $db the database connection used to execute the query. + * 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 + * @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); + } else { + foreach($this->where as $column => $values) { + if (is_array($values)) { + // TODO support composite IN for composite PK + throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.'); + } + } + $pks = [$this->where]; + } + + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + + $start = $this->offset === null ? 0 : $this->offset; + $i = 0; + $data = []; + foreach($pks as $pk) { + if (++$i > $start && ($this->limit === null || $i <= $start + $this->limit)) { + $key = $modelClass::tableName() . ':a:' . $modelClass::buildKey($pk); + $result = $db->executeCommand('HGETALL', [$key]); + if (!empty($result)) { + $data[] = $result; + if ($type === 'One' && $this->orderBy === null) { + break; + } + } + } + } + // TODO support orderBy + + switch($type) { + case 'All': + return $data; + case 'One': + return reset($data); + case 'Count': + return count($data); + case 'Column': + $column = []; + foreach($data as $dataRow) { + $row = []; + $c = count($dataRow); + for($i = 0; $i < $c; ) { + $row[$dataRow[$i++]] = $dataRow[$i++]; + } + $column[] = $row[$columnName]; + } + return $column; + case 'Sum': + $sum = 0; + foreach($data as $dataRow) { + $c = count($dataRow); + for($i = 0; $i < $c; ) { + if ($dataRow[$i++] == $columnName) { + $sum += $dataRow[$i]; + break; + } + } + } + return $sum; + case 'Average': + $sum = 0; + $count = 0; + foreach($data as $dataRow) { + $count++; + $c = count($dataRow); + for($i = 0; $i < $c; ) { + if ($dataRow[$i++] == $columnName) { + $sum += $dataRow[$i]; + break; + } + } + } + return $sum / $count; + case 'Min': + $min = null; + foreach($data as $dataRow) { + $c = count($dataRow); + for($i = 0; $i < $c; ) { + if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) { + $min = $dataRow[$i]; + break; + } + } + } + return $min; + case 'Max': + $max = null; + foreach($data as $dataRow) { + $c = count($dataRow); + for($i = 0; $i < $c; ) { + if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) { + $max = $dataRow[$i]; + break; + } + } + } + return $max; + } + throw new InvalidParamException('Unknown fetch type: ' . $type); + } +} diff --git a/extensions/redis/ActiveRecord.php b/extensions/redis/ActiveRecord.php new file mode 100644 index 0000000..46132fc --- /dev/null +++ b/extensions/redis/ActiveRecord.php @@ -0,0 +1,322 @@ + + * @since 2.0 + */ +class ActiveRecord extends \yii\db\ActiveRecord +{ + /** + * Returns the database connection used by this AR class. + * By default, the "redis" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('redis'); + } + + /** + * @inheritDoc + */ + public static function createQuery() + { + return new ActiveQuery(['modelClass' => get_called_class()]); + } + + /** + * @inheritDoc + */ + public static function createActiveRelation($config = []) + { + return new ActiveRelation($config); + } + + /** + * Returns the primary key name(s) for this AR class. + * This method should be overridden by child classes to define the primary key. + * + * Note that an array should be returned even when it is a single primary key. + * + * @return string[] the primary keys of this record. + */ + public static function primaryKey() + { + return ['id']; + } + + /** + * Returns the list of all attribute names of the model. + * This method must be overridden by child classes to define available attributes. + * @return array list of attribute names. + */ + public static function attributes() + { + throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.'); + } + + /** + * @inheritDocs + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + if ($this->beforeSave(true)) { + $db = static::getDb(); + $values = $this->getDirtyAttributes($attributes); + $pk = []; +// if ($values === []) { + foreach ($this->primaryKey() as $key) { + $pk[$key] = $values[$key] = $this->getAttribute($key); + if ($pk[$key] === null) { + $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::tableName() . ':s:' . $key]); + $this->setAttribute($key, $values[$key]); + } + } +// } + // save pk in a findall pool + $db->executeCommand('RPUSH', [static::tableName(), static::buildKey($pk)]); + + $key = static::tableName() . ':a:' . static::buildKey($pk); + // save attributes + $args = [$key]; + foreach($values as $attribute => $value) { + $args[] = $attribute; + $args[] = $value; + } + $db->executeCommand('HMSET', $args); + + $this->setOldAttributes($values); + $this->afterSave(true); + return true; + } + return false; + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['id' => 2]); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @param array $params this parameter is ignored in redis implementation. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = null, $params = []) + { + if (empty($attributes)) { + return 0; + } + $db = static::getDb(); + $n=0; + foreach(static::fetchPks($condition) as $pk) { + $newPk = $pk; + $pk = static::buildKey($pk); + $key = static::tableName() . ':a:' . $pk; + // save attributes + $args = [$key]; + foreach($attributes as $attribute => $value) { + if (isset($newPk[$attribute])) { + $newPk[$attribute] = $value; + } + $args[] = $attribute; + $args[] = $value; + } + $newPk = static::buildKey($newPk); + $newKey = static::tableName() . ':a:' . $newPk; + // rename index if pk changed + if ($newPk != $pk) { + $db->executeCommand('MULTI'); + $db->executeCommand('HMSET', $args); + $db->executeCommand('LINSERT', [static::tableName(), 'AFTER', $pk, $newPk]); + $db->executeCommand('LREM', [static::tableName(), 0, $pk]); + $db->executeCommand('RENAME', [$key, $newKey]); + $db->executeCommand('EXEC'); + } else { + $db->executeCommand('HMSET', $args); + } + $n++; + } + return $n; + } + + /** + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @param array $params this parameter is ignored in redis implementation. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = null, $params = []) + { + if (empty($counters)) { + return 0; + } + $db = static::getDb(); + $n=0; + foreach(static::fetchPks($condition) as $pk) { + $key = static::tableName() . ':a:' . static::buildKey($pk); + foreach($counters as $attribute => $value) { + $db->executeCommand('HINCRBY', [$key, $attribute, $value]); + } + $n++; + } + return $n; + } + + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll(['status' => 3]); + * ~~~ + * + * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @param array $params this parameter is ignored in redis implementation. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = null, $params = []) + { + $db = static::getDb(); + $attributeKeys = []; + $pks = static::fetchPks($condition); + $db->executeCommand('MULTI'); + foreach($pks as $pk) { + $pk = static::buildKey($pk); + $db->executeCommand('LREM', [static::tableName(), 0, $pk]); + $attributeKeys[] = static::tableName() . ':a:' . $pk; + } + if (empty($attributeKeys)) { + $db->executeCommand('EXEC'); + return 0; + } + $db->executeCommand('DEL', $attributeKeys); + $result = $db->executeCommand('EXEC'); + return end($result); + } + + private static function fetchPks($condition) + { + $query = static::createQuery(); + $query->where($condition); + $records = $query->asArray()->all(); // TODO limit fetched columns to pk + $primaryKey = static::primaryKey(); + + $pks = []; + foreach($records as $record) { + $pk = []; + foreach($primaryKey as $key) { + $pk[$key] = $record[$key]; + } + $pks[] = $pk; + } + return $pks; + } + + /** + * Builds a normalized key from a given primary key value. + * + * @param mixed $key the key to be normalized + * @return string the generated key + */ + public static function buildKey($key) + { + if (is_numeric($key)) { + return $key; + } elseif (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); + } elseif (is_array($key)) { + if (count($key) == 1) { + return self::buildKey(reset($key)); + } + ksort($key); // ensure order is always the same + $isNumeric = true; + foreach($key as $value) { + if (!is_numeric($value)) { + $isNumeric = false; + } + } + if ($isNumeric) { + return implode('-', $key); + } + } + return md5(json_encode($key)); + } + + /** + * @inheritdoc + */ + public static function getTableSchema() + { + throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord'); + } + + /** + * @inheritdoc + */ + public static function findBySql($sql, $params = []) + { + throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord'); + } + + /** + * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. + * This method will always return false as transactional operations are not supported by redis. + * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. + * @return boolean whether the specified operation is transactional in the current [[scenario]]. + */ + public function isTransactional($operation) + { + return false; + } +} diff --git a/extensions/redis/ActiveRelation.php b/extensions/redis/ActiveRelation.php new file mode 100644 index 0000000..b2f5cea --- /dev/null +++ b/extensions/redis/ActiveRelation.php @@ -0,0 +1,67 @@ + + * @since 2.0 + */ +class ActiveRelation extends ActiveQuery implements ActiveRelationInterface +{ + use ActiveRelationTrait; + + /** + * Executes a script created by [[LuaScriptBuilder]] + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @param string $type the type of the script to generate + * @param null $column + * @return array|bool|null|string + */ + protected function executeScript($db, $type, $column=null) + { + if ($this->primaryModel !== null) { + // lazy loading + if ($this->via instanceof self) { + // via pivot table + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveRelation $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + } + return parent::executeScript($db, $type, $column); + } +} diff --git a/extensions/redis/Cache.php b/extensions/redis/Cache.php new file mode 100644 index 0000000..f1a21ed --- /dev/null +++ b/extensions/redis/Cache.php @@ -0,0 +1,184 @@ + [ + * 'cache' => [ + * 'class' => 'yii\redis\Cache', + * ], + * 'redis' => [ + * 'class' => 'yii\redis\Connection', + * 'hostname' => 'localhost', + * 'port' => 6379, + * 'database' => 0, + * ] + * ], + * ] + * ~~~ + * + * @property Connection $connection The redis connection object. This property is read-only. + * + * @author Carsten Brandt + * @since 2.0 + */ +class Cache extends \yii\caching\Cache +{ + /** + * @var string the id of the application component to use as the redis connection. + * It should be configured as a [[yii\redis\Connection]]. Defaults to `redis`. + */ + public $connectionId = 'redis'; + + + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + return (bool) $connection->executeCommand('EXISTS', [$this->buildKey($key)]); + } + + /** + * @inheritDocs + */ + protected function getValue($key) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + return $connection->executeCommand('GET', [$key]); + } + + /** + * @inheritDocs + */ + protected function getValues($keys) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + $response = $connection->executeCommand('MGET', $keys); + $result = []; + $i = 0; + foreach ($keys as $key) { + $result[$key] = $response[$i++]; + } + return $result; + } + + /** + * @inheritDocs + */ + protected function setValue($key, $value, $expire) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + if ($expire == 0) { + return (bool) $connection->executeCommand('SET', [$key, $value]); + } else { + $expire = (int) ($expire * 1000); + return (bool) $connection->executeCommand('SET', [$key, $value, 'PX', $expire]); + } + } + + /** + * @inheritDocs + */ + protected function setValues($data, $expire) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + + $args = []; + foreach($data as $key => $value) { + $args[] = $key; + $args[] = $value; + } + + $failedKeys = []; + if ($expire == 0) { + $connection->executeCommand('MSET', $args); + } else { + $expire = (int) ($expire * 1000); + $connection->executeCommand('MULTI'); + $connection->executeCommand('MSET', $args); + $index = []; + foreach ($data as $key => $value) { + $connection->executeCommand('PEXPIRE', [$key, $expire]); + $index[] = $key; + } + $result = $connection->executeCommand('EXEC'); + array_shift($result); + foreach($result as $i => $r) { + if ($r != 1) { + $failedKeys[] = $index[$i]; + } + } + } + return $failedKeys; + } + + /** + * @inheritDocs + */ + protected function addValue($key, $value, $expire) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + if ($expire == 0) { + return (bool) $connection->executeCommand('SET', [$key, $value, 'NX']); + } else { + $expire = (int) ($expire * 1000); + return (bool) $connection->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']); + } + } + + /** + * @inheritDocs + */ + protected function deleteValue($key) + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + return (bool) $connection->executeCommand('DEL', [$key]); + } + + /** + * @inheritDocs + */ + protected function flushValues() + { + /** @var Connection $connection */ + $connection = \Yii::$app->getComponent($this->connectionId); + return $connection->executeCommand('FLUSHDB'); + } +} diff --git a/extensions/redis/Connection.php b/extensions/redis/Connection.php new file mode 100644 index 0000000..cea559f --- /dev/null +++ b/extensions/redis/Connection.php @@ -0,0 +1,406 @@ + + * @since 2.0 + */ +class Connection extends Component +{ + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; + + /** + * @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'. + */ + public $hostname = 'localhost'; + /** + * @var int the port to use for connecting to the redis server. Default port is 6379. + */ + public $port = 6379; + /** + * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send. + * See http://redis.io/commands/auth + */ + public $password; + /** + * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0. + */ + 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") + */ + public $connectionTimeout = null; + /** + * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. + */ + public $dataTimeout = null; + + /** + * @var array List of available redis commands http://redis.io/commands + */ + public $redisCommands = [ + 'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available + 'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available + 'CLIENT KILL', // ip:port Kill the connection of a client + 'CLIENT LIST', // Get the list of client connections + 'CLIENT GETNAME', // Get the current connection name + 'CLIENT SETNAME', // connection-name Set the current connection name + 'CONFIG GET', // parameter Get the value of a configuration parameter + 'CONFIG SET', // parameter value Set a configuration parameter to the given value + 'CONFIG RESETSTAT', // Reset the stats returned by INFO + 'DBSIZE', // Return the number of keys in the selected database + 'DEBUG OBJECT', // key Get debugging information about a key + 'DEBUG SEGFAULT', // Make the server crash + 'DECR', // key Decrement the integer value of a key by one + 'DECRBY', // key decrement Decrement the integer value of a key by the given number + 'DEL', // key [key ...] Delete a key + 'DISCARD', // Discard all commands issued after MULTI + 'DUMP', // key Return a serialized version of the value stored at the specified key. + 'ECHO', // message Echo the given string + 'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EXEC', // Execute all commands issued after MULTI + 'EXISTS', // key Determine if a key exists + 'EXPIRE', // key seconds Set a key's time to live in seconds + 'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp + 'FLUSHALL', // Remove all keys from all databases + 'FLUSHDB', // Remove all keys from the current database + 'GET', // key Get the value of a key + 'GETBIT', // key offset Returns the bit value at offset in the string value stored at key + 'GETRANGE', // key start end Get a substring of the string stored at a key + 'GETSET', // key value Set the string value of a key and return its old value + 'HDEL', // key field [field ...] Delete one or more hash fields + 'HEXISTS', // key field Determine if a hash field exists + 'HGET', // key field Get the value of a hash field + 'HGETALL', // key Get all the fields and values in a hash + 'HINCRBY', // key field increment Increment the integer value of a hash field by the given number + 'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount + 'HKEYS', // key Get all the fields in a hash + 'HLEN', // key Get the number of fields in a hash + 'HMGET', // key field [field ...] Get the values of all the given hash fields + 'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values + 'HSET', // key field value Set the string value of a hash field + 'HSETNX', // key field value Set the value of a hash field, only if the field does not exist + 'HVALS', // key Get all the values in a hash + 'INCR', // key Increment the integer value of a key by one + 'INCRBY', // key increment Increment the integer value of a key by the given amount + 'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount + 'INFO', // [section] Get information and statistics about the server + 'KEYS', // pattern Find all keys matching the given pattern + 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk + 'LINDEX', // key index Get an element from a list by its index + 'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list + 'LLEN', // key Get the length of a list + 'LPOP', // key Remove and get the first element in a list + 'LPUSH', // key value [value ...] Prepend one or multiple values to a list + 'LPUSHX', // key value Prepend a value to a list, only if the list exists + 'LRANGE', // key start stop Get a range of elements from a list + 'LREM', // key count value Remove elements from a list + 'LSET', // key index value Set the value of an element in a list by its index + 'LTRIM', // key start stop Trim a list to the specified range + 'MGET', // key [key ...] Get the values of all the given keys + 'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one. + 'MONITOR', // Listen for all requests received by the server in real time + 'MOVE', // key db Move a key to another database + 'MSET', // key value [key value ...] Set multiple keys to multiple values + 'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist + 'MULTI', // Mark the start of a transaction block + 'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects + 'PERSIST', // key Remove the expiration from a key + 'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds + 'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds + 'PING', // Ping the server + 'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key + 'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns + 'PTTL', // key Get the time to live for a key in milliseconds + 'PUBLISH', // channel message Post a message to a channel + 'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns + 'QUIT', // Close the connection + 'RANDOMKEY', // Return a random key from the keyspace + 'RENAME', // key newkey Rename a key + 'RENAMENX', // key newkey Rename a key, only if the new key does not exist + 'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP. + 'RPOP', // key Remove and get the last element in a list + 'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it + 'RPUSH', // key value [value ...] Append one or multiple values to a list + 'RPUSHX', // key value Append a value to a list, only if the list exists + 'SADD', // key member [member ...] Add one or more members to a set + 'SAVE', // Synchronously save the dataset to disk + 'SCARD', // key Get the number of members in a set + 'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache. + 'SCRIPT FLUSH', // Remove all the scripts from the script cache. + 'SCRIPT KILL', // Kill the script currently in execution. + 'SCRIPT LOAD', // script Load the specified Lua script into the script cache. + 'SDIFF', // key [key ...] Subtract multiple sets + 'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key + 'SELECT', // index Change the selected database for the current connection + 'SET', // key value Set the string value of a key + 'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key + 'SETEX', // key seconds value Set the value and expiration of a key + 'SETNX', // key value Set the value of a key, only if the key does not exist + 'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset + 'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server + 'SINTER', // key [key ...] Intersect multiple sets + 'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key + 'SISMEMBER', // key member Determine if a given value is a member of a set + 'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master + 'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log + 'SMEMBERS', // key Get all the members in a set + 'SMOVE', // source destination member Move a member from one set to another + 'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set + 'SPOP', // key Remove and return a random member from a set + 'SRANDMEMBER', // key [count] Get one or multiple random members from a set + 'SREM', // key member [member ...] Remove one or more members from a set + 'STRLEN', // key Get the length of the value stored in a key + 'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels + 'SUNION', // key [key ...] Add multiple sets + 'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key + 'SYNC', // Internal command used for replication + 'TIME', // Return the current server time + 'TTL', // key Get the time to live for a key + 'TYPE', // key Determine the type stored at key + 'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels + 'UNWATCH', // Forget about all watched keys + 'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block + 'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists + 'ZCARD', // key Get the number of members in a sorted set + 'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values + 'ZINCRBY', // key increment member Increment the score of a member in a sorted set + 'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key + 'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index + 'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score + 'ZRANK', // key member Determine the index of a member in a sorted set + 'ZREM', // key member [member ...] Remove one or more members from a sorted set + 'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes + 'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores + 'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low + 'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low + '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 + ]; + /** + * @var resource redis socket connection + */ + private $_socket; + + /** + * Closes the connection when this component is being serialized. + * @return array + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->_socket !== null; + } + + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->_socket !== null) { + return; + } + $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; + \Yii::trace('Opening redis DB connection: ' . $connection, __CLASS__); + $this->_socket = @stream_socket_client( + '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)); + } + if ($this->password !== null) { + $this->executeCommand('AUTH', [$this->password]); + } + $this->executeCommand('SELECT', [$this->database]); + $this->initConnection(); + } else { + \Yii::error("Failed to open DB connection ($connection): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); + $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; + throw new Exception($message, $errorDescription, (int)$errorNumber); + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->_socket !== null) { + $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; + \Yii::trace('Closing DB connection: ' . $connection, __CLASS__); + $this->executeCommand('QUIT'); + stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); + $this->_socket = null; + } + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + return 'redis'; + } + + /** + * @return LuaScriptBuilder + */ + public function getLuaScriptBuilder() + { + return new LuaScriptBuilder(); + } + + /** + * + * @param string $name + * @param array $params + * @return mixed + */ + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) { + return $this->executeCommand($name, $params); + } else { + return parent::__call($name, $params); + } + } + + /** + * Executes a redis command. + * For a list of available commands and their parameters see http://redis.io/commands. + * + * @param string $name the name of the command + * @param array $params list of parameters for the command + * @return array|bool|null|string Dependend on the executed command this method + * will return different data types: + * + * - `true` for commands that return "status reply". + * - `string` for commands that return "integer reply" + * as the value is in the range of a signed 64 bit integer. + * - `string` or `null` for commands that return "bulk reply". + * - `array` for commands that return "Multi-bulk replies". + * + * See [redis protocol description](http://redis.io/topics/protocol) + * for details on the mentioned reply types. + * @trows Exception for commands that return [error reply](http://redis.io/topics/protocol#error-reply). + */ + public function executeCommand($name, $params=[]) + { + $this->open(); + + array_unshift($params, $name); + $command = '*' . count($params) . "\r\n"; + foreach($params as $arg) { + $command .= '$' . mb_strlen($arg, '8bit') . "\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) { + 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 + return true; + 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 = fread($this->_socket, $line + 2)) === 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); + } + } +} diff --git a/extensions/redis/LICENSE.md b/extensions/redis/LICENSE.md new file mode 100644 index 0000000..e98f03d --- /dev/null +++ b/extensions/redis/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/redis/LuaScriptBuilder.php b/extensions/redis/LuaScriptBuilder.php new file mode 100644 index 0000000..81dff3f --- /dev/null +++ b/extensions/redis/LuaScriptBuilder.php @@ -0,0 +1,365 @@ + + * @since 2.0 + */ +class LuaScriptBuilder extends \yii\base\Object +{ + /** + * Builds a Lua script for finding a list of records + * @param ActiveQuery $query the query used to build the script + * @return string + */ + public function buildAll($query) + { + // TODO add support for orderBy + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks'); + } + + /** + * Builds a Lua script for finding one record + * @param ActiveQuery $query the query used to build the script + * @return string + */ + public function buildOne($query) + { + // TODO add support for orderBy + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks'); + } + + /** + * Builds a Lua script for finding a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildColumn($query, $column) + { + // TODO add support for orderBy and indexBy + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks'); + } + + /** + * Builds a Lua script for getting count of records + * @param ActiveQuery $query the query used to build the script + * @return string + */ + public function buildCount($query) + { + return $this->build($query, 'n=n+1', 'n'); + } + + /** + * Builds a Lua script for finding the sum of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildSum($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n'); + } + + /** + * Builds a Lua script for finding the average of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildAverage($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n'); + } + + /** + * Builds a Lua script for finding the min value of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildMin($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass; + $key = $this->quoteValue($modelClass::tableName() . ':a:'); + return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v'); + } + + /** + * @param ActiveQuery $query the query used to build the script + * @param string $buildResult the lua script for building the result + * @param string $return the lua variable that should be returned + * @return string + */ + private function build($query, $buildResult, $return) + { + if (!empty($query->orderBy)) { + throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); + } + + $columns = []; + if ($query->where !== null) { + $condition = $this->buildCondition($query->where, $columns); + } else { + $condition = 'true'; + } + + $start = $query->offset === null ? 0 : $query->offset; + $limitCondition = 'i>' . $start . ($query->limit === null ? '' : ' and i<=' . ($start + $query->limit)); + + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::tableName()); + $loadColumnValues = ''; + foreach($columns as $column => $alias) { + $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, '$column')\n"; + } + + return << 'buildAndCondition', + 'or' => 'buildAndCondition', + 'between' => 'buildBetweenCondition', + 'not between' => 'buildBetweenCondition', + 'in' => 'buildInCondition', + 'not in' => 'buildInCondition', + 'like' => 'buildLikeCondition', + 'not like' => 'buildLikeCondition', + 'or like' => 'buildLikeCondition', + 'or not like' => 'buildLikeCondition', + ]; + + if (!is_array($condition)) { + throw new NotSupportedException('Where condition must be an array in redis ActiveRecord.'); + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtolower($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition, $columns); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + return $this->buildHashCondition($condition, $columns); + } + } + + private function buildHashCondition($condition, &$columns) + { + $parts = []; + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('in', [$column, $value], $columns); + } else { + $column = $this->addColumn($column, $columns); + if ($value === null) { + $parts[] = "$column==nil"; + } elseif ($value instanceof Expression) { + $parts[] = "$column==" . $value->expression; + } else { + $value = $this->quoteValue($value); + $parts[] = "$column==$value"; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') and (', $parts) . ')'; + } + + private function buildAndCondition($operator, $operands, &$columns) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $columns); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$columns) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + $value1 = $this->quoteValue($value1); + $value2 = $this->quoteValue($value2); + $column = $this->addColumn($column, $columns); + return "$column >= $value1 and $column <= $value2"; + } + + private function buildInCondition($operator, $operands, &$columns) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if (empty($values) || $column === []) { + return $operator === 'in' ? 'false' : 'true'; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $columns); + } elseif (is_array($column)) { + $column = reset($column); + } + $columnAlias = $this->addColumn($column, $columns); + $parts = []; + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $parts[] = "$columnAlias==nil"; + } elseif ($value instanceof Expression) { + $parts[] = "$columnAlias==" . $value->expression; + } else { + $value = $this->quoteValue($value); + $parts[] = "$columnAlias==$value"; + } + } + $operator = $operator === 'in' ? '' : 'not '; + return "$operator(" . implode(' or ', $parts) . ')'; + } + + protected function buildCompositeInCondition($operator, $inColumns, $values, &$columns) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($inColumns as $column) { + $column = $this->addColumn($column, $columns); + if (isset($value[$column])) { + $vs[] = "$column==" . $this->quoteValue($value[$column]); + } else { + $vs[] = "$column==nil"; + } + } + $vss[] = '(' . implode(' and ', $vs) . ')'; + } + $operator = $operator === 'in' ? '' : 'not '; + return "$operator(" . implode(' or ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$columns) + { + throw new NotSupportedException('LIKE conditions are not suppoerted by redis ActiveRecord.'); + } +} diff --git a/extensions/redis/README.md b/extensions/redis/README.md new file mode 100644 index 0000000..28cecf1 --- /dev/null +++ b/extensions/redis/README.md @@ -0,0 +1,91 @@ +Redis Cache and ActiveRecord for Yii 2 +====================================== + +This extension provides the [redis](http://redis.io/) key-value store support for the Yii2 framework. +It includes a `Cache` class and implents the `ActiveRecord` pattern that allows you to store active +records in redis. + +To use this extension, you have to configure the Connection class in your application configuration: + +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\Connection', + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + ], + ] +]; +``` + +To use the `Cache` component, you also have to configure the `cache` component to be `yii\redis\Cache`: + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require yiisoft/yii2-redis "*" +``` + +or add + +```json +"yiisoft/yii2-redis": "*" +``` + +to the require section of your composer.json. + + +Using the redis ActiveRecord +---------------------------- + +For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). + +For defining a redis ActiveRecord class your record class needs to extend from `yii\redis\ActiveRecord` and +implement at least the `attributes()` method to define the attributes of the record. +A primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified. +The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do +not specify your own primary key. + +The following is an example model called `Customer`: + +```php +class Customer extends \yii\redis\ActiveRecord +{ + public function attributes() + { + return ['id', 'name', 'address', 'registration_date']; + } +} +``` + +The general usage of redis ActiveRecord is very similar to the database ActiveRecord as described in the +[guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). +It supports the same interface and features except the following limitations: + +- As redis does not support SQL the query API is limited to the following methods: + `where()`, `limit()`, `offset()`, `orderBy()` and `indexBy()`. + (orderBy() is not yet implemented: [#1305](https://github.com/yiisoft/yii2/issues/1305)) +- `via`-relations can not be defined via a table as there are not tables in redis. You can only define relations via other records. + +It is also possible to define relations from redis ActiveRecords to normal ActiveRecord classes and vice versa. \ No newline at end of file diff --git a/extensions/redis/composer.json b/extensions/redis/composer.json new file mode 100644 index 0000000..fb5065b --- /dev/null +++ b/extensions/redis/composer.json @@ -0,0 +1,27 @@ +{ + "name": "yiisoft/yii2-redis", + "description": "Redis Cache and ActiveRecord for the Yii framework", + "keywords": ["yii", "redis", "active-record", "cache"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aredis", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc" + } + ], + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\redis\\": "" } + }, + "target-dir": "yii/redis" +} diff --git a/extensions/sphinx/composer.json b/extensions/sphinx/composer.json index 0331667..decac2f 100644 --- a/extensions/sphinx/composer.json +++ b/extensions/sphinx/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-sphinx", "description": "Sphinx full text search engine extension for the Yii framework", - "keywords": ["yii", "sphinx", "search", "fulltext"], + "keywords": ["yii", "sphinx", "active-record", "search", "fulltext"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/framework/yii/caching/RedisCache.php b/framework/yii/caching/RedisCache.php deleted file mode 100644 index b64f000..0000000 --- a/framework/yii/caching/RedisCache.php +++ /dev/null @@ -1,218 +0,0 @@ - [ - * 'cache' => [ - * 'class' => 'RedisCache', - * 'hostname' => 'localhost', - * 'port' => 6379, - * 'database' => 0, - * ], - * ], - * ] - * ~~~ - * - * @property Connection $connection The redis connection object. This property is read-only. - * - * @author Carsten Brandt - * @since 2.0 - */ -class RedisCache extends Cache -{ - /** - * @var string hostname to use for connecting to the redis server. Defaults to 'localhost'. - */ - public $hostname = 'localhost'; - /** - * @var int the port to use for connecting to the redis server. Default port is 6379. - */ - public $port = 6379; - /** - * @var string the password to use to authenticate with the redis server. If not set, no AUTH command will be sent. - */ - public $password; - /** - * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0. - */ - 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") - */ - public $connectionTimeout = null; - /** - * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. - */ - public $dataTimeout = null; - /** - * @var Connection the redis connection - */ - private $_connection; - - - /** - * Initializes the cache component by establishing a connection to the redis server. - */ - public function init() - { - parent::init(); - $this->getConnection(); - } - - /** - * Returns the redis connection object. - * Establishes a connection to the redis server if it does not already exists. - * @return Connection the redis connection object. - */ - public function getConnection() - { - if ($this->_connection === null) { - $this->_connection = new Connection([ - 'dsn' => 'redis://' . $this->hostname . ':' . $this->port . '/' . $this->database, - 'password' => $this->password, - 'connectionTimeout' => $this->connectionTimeout, - 'dataTimeout' => $this->dataTimeout, - ]); - } - return $this->_connection; - } - - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - return (bool) $this->_connection->executeCommand('EXISTS', [$this->buildKey($key)]); - } - - /** - * @inheritDocs - */ - protected function getValue($key) - { - return $this->_connection->executeCommand('GET', [$key]); - } - - /** - * @inheritDocs - */ - protected function getValues($keys) - { - $response = $this->_connection->executeCommand('MGET', $keys); - $result = []; - $i = 0; - foreach ($keys as $key) { - $result[$key] = $response[$i++]; - } - return $result; - } - - /** - * @inheritDocs - */ - protected function setValue($key, $value, $expire) - { - if ($expire == 0) { - return (bool) $this->_connection->executeCommand('SET', [$key, $value]); - } else { - $expire = (int) ($expire * 1000); - return (bool) $this->_connection->executeCommand('SET', [$key, $value, 'PX', $expire]); - } - } - - /** - * @inheritDocs - */ - protected function setValues($data, $expire) - { - $args = []; - foreach($data as $key => $value) { - $args[] = $key; - $args[] = $value; - } - - $failedKeys = []; - if ($expire == 0) { - $this->_connection->executeCommand('MSET', $args); - } else { - $expire = (int) ($expire * 1000); - $this->_connection->executeCommand('MULTI'); - $this->_connection->executeCommand('MSET', $args); - $index = []; - foreach ($data as $key => $value) { - $this->_connection->executeCommand('PEXPIRE', [$key, $expire]); - $index[] = $key; - } - $result = $this->_connection->executeCommand('EXEC'); - array_shift($result); - foreach($result as $i => $r) { - if ($r != 1) { - $failedKeys[] = $index[$i]; - } - } - } - return $failedKeys; - } - - /** - * @inheritDocs - */ - protected function addValue($key, $value, $expire) - { - if ($expire == 0) { - return (bool) $this->_connection->executeCommand('SET', [$key, $value, 'NX']); - } else { - $expire = (int) ($expire * 1000); - return (bool) $this->_connection->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']); - } - } - - /** - * @inheritDocs - */ - protected function deleteValue($key) - { - return (bool) $this->_connection->executeCommand('DEL', [$key]); - } - - /** - * @inheritDocs - */ - protected function flushValues() - { - return $this->_connection->executeCommand('FLUSHDB'); - } -} diff --git a/framework/yii/classes.php b/framework/yii/classes.php index 9f39ee9..c8b2935 100644 --- a/framework/yii/classes.php +++ b/framework/yii/classes.php @@ -61,7 +61,6 @@ return [ 'yii\caching\GroupDependency' => YII_PATH . '/caching/GroupDependency.php', 'yii\caching\MemCache' => YII_PATH . '/caching/MemCache.php', 'yii\caching\MemCacheServer' => YII_PATH . '/caching/MemCacheServer.php', - 'yii\caching\RedisCache' => YII_PATH . '/caching/RedisCache.php', 'yii\caching\WinCache' => YII_PATH . '/caching/WinCache.php', 'yii\caching\XCache' => YII_PATH . '/caching/XCache.php', 'yii\caching\ZendDataCache' => YII_PATH . '/caching/ZendDataCache.php', @@ -168,8 +167,6 @@ return [ 'yii\rbac\Item' => YII_PATH . '/rbac/Item.php', 'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php', 'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php', - 'yii\redis\Connection' => YII_PATH . '/redis/Connection.php', - 'yii\redis\Transaction' => YII_PATH . '/redis/Transaction.php', 'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php', 'yii\test\DbFixtureManager' => YII_PATH . '/test/DbFixtureManager.php', 'yii\test\DbTestTrait' => YII_PATH . '/test/DbTestTrait.php', diff --git a/framework/yii/redis/ActiveQuery.php b/framework/yii/redis/ActiveQuery.php deleted file mode 100644 index 2174901..0000000 --- a/framework/yii/redis/ActiveQuery.php +++ /dev/null @@ -1,382 +0,0 @@ -with('orders')->asArray()->all(); - * ~~~ - * - * @author Carsten Brandt - * @since 2.0 - */ -class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface -{ - use QueryTrait; - use ActiveQueryTrait; - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. - */ - 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($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - return $models; - } else { - return []; - } - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - // TODO add support for orderBy - $data = $this->executeScript($db, 'One'); - if (empty($data)) { - return null; - } - $row = []; - $c = count($data); - for($i = 0; $i < $c; ) { - $row[$data[$i++]] = $data[$i++]; - } - if ($this->asArray) { - $model = $row; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::create($row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - return $model; - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - if ($this->offset === null && $this->limit === null && $this->where === null) { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - return $db->executeCommand('LLEN', [$modelClass::tableName()]); - } else { - return $this->executeScript($db, 'Count'); - } - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - return $this->one($db) !== null; - } - - /** - * Executes the query and returns the first column of the result. - * @param string $column name of the column to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($column, $db = null) - { - // TODO add support for orderBy - return $this->executeScript($db, 'Column', $column); - } - - /** - * Returns the number of records. - * @param string $column the column to sum up - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer number of records - */ - public function sum($column, $db = null) - { - return $this->executeScript($db, 'Sum', $column); - } - - /** - * Returns the average of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the average of the specified column values. - */ - public function average($column, $db = null) - { - return $this->executeScript($db, 'Average', $column); - } - - /** - * Returns the minimum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the minimum of the specified column values. - */ - public function min($column, $db = null) - { - return $this->executeScript($db, 'Min', $column); - } - - /** - * Returns the maximum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the maximum of the specified column values. - */ - public function max($column, $db = null) - { - return $this->executeScript($db, 'Max', $column); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the specified attribute in the first record of the query results. - * @param string $attribute name of the attribute to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return string the value of the specified attribute in the first record of the query result. - * Null is returned if the query result is empty. - */ - public function scalar($attribute, $db = null) - { - $record = $this->one($db); - if ($record !== null) { - return $record->$attribute; - } else { - return null; - } - } - - - /** - * Executes a script created by [[LuaScriptBuilder]] - * @param Connection $db the database connection used to execute the query. - * 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 - */ - protected function executeScript($db, $type, $columnName = null) - { - if (!empty($this->orderBy)) { - throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); - } - - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - - if ($db === null) { - $db = $modelClass::getDb(); - } - - // find by primary key if possible. This is much faster than scanning all records - if (is_array($this->where) && !isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where))) { - return $this->findByPk($db, $type, $columnName); - } - - $method = 'build' . $type; - $script = $db->getLuaScriptBuilder()->$method($this, $columnName); - return $db->executeCommand('EVAL', [$script, 0]); - } - - /** - * Fetch by pk if possible as this is much faster - * @param Connection $db the database connection used to execute the query. - * 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 - * @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); - } else { - foreach($this->where as $column => $values) { - if (is_array($values)) { - // TODO support composite IN for composite PK - throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.'); - } - } - $pks = [$this->where]; - } - - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - - $start = $this->offset === null ? 0 : $this->offset; - $i = 0; - $data = []; - foreach($pks as $pk) { - if (++$i > $start && ($this->limit === null || $i <= $start + $this->limit)) { - $key = $modelClass::tableName() . ':a:' . $modelClass::buildKey($pk); - $result = $db->executeCommand('HGETALL', [$key]); - if (!empty($result)) { - $data[] = $result; - if ($type === 'One' && $this->orderBy === null) { - break; - } - } - } - } - // TODO support orderBy - - switch($type) { - case 'All': - return $data; - case 'One': - return reset($data); - case 'Count': - return count($data); - case 'Column': - $column = []; - foreach($data as $dataRow) { - $row = []; - $c = count($dataRow); - for($i = 0; $i < $c; ) { - $row[$dataRow[$i++]] = $dataRow[$i++]; - } - $column[] = $row[$columnName]; - } - return $column; - case 'Sum': - $sum = 0; - foreach($data as $dataRow) { - $c = count($dataRow); - for($i = 0; $i < $c; ) { - if ($dataRow[$i++] == $columnName) { - $sum += $dataRow[$i]; - break; - } - } - } - return $sum; - case 'Average': - $sum = 0; - $count = 0; - foreach($data as $dataRow) { - $count++; - $c = count($dataRow); - for($i = 0; $i < $c; ) { - if ($dataRow[$i++] == $columnName) { - $sum += $dataRow[$i]; - break; - } - } - } - return $sum / $count; - case 'Min': - $min = null; - foreach($data as $dataRow) { - $c = count($dataRow); - for($i = 0; $i < $c; ) { - if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) { - $min = $dataRow[$i]; - break; - } - } - } - return $min; - case 'Max': - $max = null; - foreach($data as $dataRow) { - $c = count($dataRow); - for($i = 0; $i < $c; ) { - if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) { - $max = $dataRow[$i]; - break; - } - } - } - return $max; - } - throw new InvalidParamException('Unknown fetch type: ' . $type); - } -} diff --git a/framework/yii/redis/ActiveRecord.php b/framework/yii/redis/ActiveRecord.php deleted file mode 100644 index 46132fc..0000000 --- a/framework/yii/redis/ActiveRecord.php +++ /dev/null @@ -1,322 +0,0 @@ - - * @since 2.0 - */ -class ActiveRecord extends \yii\db\ActiveRecord -{ - /** - * Returns the database connection used by this AR class. - * By default, the "redis" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getComponent('redis'); - } - - /** - * @inheritDoc - */ - public static function createQuery() - { - return new ActiveQuery(['modelClass' => get_called_class()]); - } - - /** - * @inheritDoc - */ - public static function createActiveRelation($config = []) - { - return new ActiveRelation($config); - } - - /** - * Returns the primary key name(s) for this AR class. - * This method should be overridden by child classes to define the primary key. - * - * Note that an array should be returned even when it is a single primary key. - * - * @return string[] the primary keys of this record. - */ - public static function primaryKey() - { - return ['id']; - } - - /** - * Returns the list of all attribute names of the model. - * This method must be overridden by child classes to define available attributes. - * @return array list of attribute names. - */ - public static function attributes() - { - throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.'); - } - - /** - * @inheritDocs - */ - public function insert($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - if ($this->beforeSave(true)) { - $db = static::getDb(); - $values = $this->getDirtyAttributes($attributes); - $pk = []; -// if ($values === []) { - foreach ($this->primaryKey() as $key) { - $pk[$key] = $values[$key] = $this->getAttribute($key); - if ($pk[$key] === null) { - $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::tableName() . ':s:' . $key]); - $this->setAttribute($key, $values[$key]); - } - } -// } - // save pk in a findall pool - $db->executeCommand('RPUSH', [static::tableName(), static::buildKey($pk)]); - - $key = static::tableName() . ':a:' . static::buildKey($pk); - // save attributes - $args = [$key]; - foreach($values as $attribute => $value) { - $args[] = $attribute; - $args[] = $value; - } - $db->executeCommand('HMSET', $args); - - $this->setOldAttributes($values); - $this->afterSave(true); - return true; - } - return false; - } - - /** - * Updates the whole table using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], ['id' => 2]); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @param array $params this parameter is ignored in redis implementation. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = null, $params = []) - { - if (empty($attributes)) { - return 0; - } - $db = static::getDb(); - $n=0; - foreach(static::fetchPks($condition) as $pk) { - $newPk = $pk; - $pk = static::buildKey($pk); - $key = static::tableName() . ':a:' . $pk; - // save attributes - $args = [$key]; - foreach($attributes as $attribute => $value) { - if (isset($newPk[$attribute])) { - $newPk[$attribute] = $value; - } - $args[] = $attribute; - $args[] = $value; - } - $newPk = static::buildKey($newPk); - $newKey = static::tableName() . ':a:' . $newPk; - // rename index if pk changed - if ($newPk != $pk) { - $db->executeCommand('MULTI'); - $db->executeCommand('HMSET', $args); - $db->executeCommand('LINSERT', [static::tableName(), 'AFTER', $pk, $newPk]); - $db->executeCommand('LREM', [static::tableName(), 0, $pk]); - $db->executeCommand('RENAME', [$key, $newKey]); - $db->executeCommand('EXEC'); - } else { - $db->executeCommand('HMSET', $args); - } - $n++; - } - return $n; - } - - /** - * Updates the whole table using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @param array $params this parameter is ignored in redis implementation. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = null, $params = []) - { - if (empty($counters)) { - return 0; - } - $db = static::getDb(); - $n=0; - foreach(static::fetchPks($condition) as $pk) { - $key = static::tableName() . ':a:' . static::buildKey($pk); - foreach($counters as $attribute => $value) { - $db->executeCommand('HINCRBY', [$key, $attribute, $value]); - } - $n++; - } - return $n; - } - - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll(['status' => 3]); - * ~~~ - * - * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @param array $params this parameter is ignored in redis implementation. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = null, $params = []) - { - $db = static::getDb(); - $attributeKeys = []; - $pks = static::fetchPks($condition); - $db->executeCommand('MULTI'); - foreach($pks as $pk) { - $pk = static::buildKey($pk); - $db->executeCommand('LREM', [static::tableName(), 0, $pk]); - $attributeKeys[] = static::tableName() . ':a:' . $pk; - } - if (empty($attributeKeys)) { - $db->executeCommand('EXEC'); - return 0; - } - $db->executeCommand('DEL', $attributeKeys); - $result = $db->executeCommand('EXEC'); - return end($result); - } - - private static function fetchPks($condition) - { - $query = static::createQuery(); - $query->where($condition); - $records = $query->asArray()->all(); // TODO limit fetched columns to pk - $primaryKey = static::primaryKey(); - - $pks = []; - foreach($records as $record) { - $pk = []; - foreach($primaryKey as $key) { - $pk[$key] = $record[$key]; - } - $pks[] = $pk; - } - return $pks; - } - - /** - * Builds a normalized key from a given primary key value. - * - * @param mixed $key the key to be normalized - * @return string the generated key - */ - public static function buildKey($key) - { - if (is_numeric($key)) { - return $key; - } elseif (is_string($key)) { - return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); - } elseif (is_array($key)) { - if (count($key) == 1) { - return self::buildKey(reset($key)); - } - ksort($key); // ensure order is always the same - $isNumeric = true; - foreach($key as $value) { - if (!is_numeric($value)) { - $isNumeric = false; - } - } - if ($isNumeric) { - return implode('-', $key); - } - } - return md5(json_encode($key)); - } - - /** - * @inheritdoc - */ - public static function getTableSchema() - { - throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord'); - } - - /** - * @inheritdoc - */ - public static function findBySql($sql, $params = []) - { - throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord'); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * This method will always return false as transactional operations are not supported by redis. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - return false; - } -} diff --git a/framework/yii/redis/ActiveRelation.php b/framework/yii/redis/ActiveRelation.php deleted file mode 100644 index b2f5cea..0000000 --- a/framework/yii/redis/ActiveRelation.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @since 2.0 - */ -class ActiveRelation extends ActiveQuery implements ActiveRelationInterface -{ - use ActiveRelationTrait; - - /** - * Executes a script created by [[LuaScriptBuilder]] - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @param string $type the type of the script to generate - * @param null $column - * @return array|bool|null|string - */ - protected function executeScript($db, $type, $column=null) - { - if ($this->primaryModel !== null) { - // lazy loading - if ($this->via instanceof self) { - // via pivot table - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveRelation $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } - return parent::executeScript($db, $type, $column); - } -} diff --git a/framework/yii/redis/Connection.php b/framework/yii/redis/Connection.php deleted file mode 100644 index 371b8bc..0000000 --- a/framework/yii/redis/Connection.php +++ /dev/null @@ -1,405 +0,0 @@ - - * @since 2.0 - */ -class Connection extends Component -{ - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; - - /** - * @var string the Data Source Name, or DSN, contains the information required to connect to the database. - * 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 password for establishing DB connection. Defaults to null meaning no AUTH command is send. - * See http://redis.io/commands/auth - */ - public $password; - /** - * @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") - */ - public $connectionTimeout = null; - /** - * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. - */ - public $dataTimeout = null; - - /** - * @var array List of available redis commands http://redis.io/commands - */ - public $redisCommands = [ - 'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available - 'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available - 'CLIENT KILL', // ip:port Kill the connection of a client - 'CLIENT LIST', // Get the list of client connections - 'CLIENT GETNAME', // Get the current connection name - 'CLIENT SETNAME', // connection-name Set the current connection name - 'CONFIG GET', // parameter Get the value of a configuration parameter - 'CONFIG SET', // parameter value Set a configuration parameter to the given value - 'CONFIG RESETSTAT', // Reset the stats returned by INFO - 'DBSIZE', // Return the number of keys in the selected database - 'DEBUG OBJECT', // key Get debugging information about a key - 'DEBUG SEGFAULT', // Make the server crash - 'DECR', // key Decrement the integer value of a key by one - 'DECRBY', // key decrement Decrement the integer value of a key by the given number - 'DEL', // key [key ...] Delete a key - 'DISCARD', // Discard all commands issued after MULTI - 'DUMP', // key Return a serialized version of the value stored at the specified key. - 'ECHO', // message Echo the given string - 'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side - 'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side - 'EXEC', // Execute all commands issued after MULTI - 'EXISTS', // key Determine if a key exists - 'EXPIRE', // key seconds Set a key's time to live in seconds - 'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp - 'FLUSHALL', // Remove all keys from all databases - 'FLUSHDB', // Remove all keys from the current database - 'GET', // key Get the value of a key - 'GETBIT', // key offset Returns the bit value at offset in the string value stored at key - 'GETRANGE', // key start end Get a substring of the string stored at a key - 'GETSET', // key value Set the string value of a key and return its old value - 'HDEL', // key field [field ...] Delete one or more hash fields - 'HEXISTS', // key field Determine if a hash field exists - 'HGET', // key field Get the value of a hash field - 'HGETALL', // key Get all the fields and values in a hash - 'HINCRBY', // key field increment Increment the integer value of a hash field by the given number - 'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount - 'HKEYS', // key Get all the fields in a hash - 'HLEN', // key Get the number of fields in a hash - 'HMGET', // key field [field ...] Get the values of all the given hash fields - 'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values - 'HSET', // key field value Set the string value of a hash field - 'HSETNX', // key field value Set the value of a hash field, only if the field does not exist - 'HVALS', // key Get all the values in a hash - 'INCR', // key Increment the integer value of a key by one - 'INCRBY', // key increment Increment the integer value of a key by the given amount - 'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount - 'INFO', // [section] Get information and statistics about the server - 'KEYS', // pattern Find all keys matching the given pattern - 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk - 'LINDEX', // key index Get an element from a list by its index - 'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list - 'LLEN', // key Get the length of a list - 'LPOP', // key Remove and get the first element in a list - 'LPUSH', // key value [value ...] Prepend one or multiple values to a list - 'LPUSHX', // key value Prepend a value to a list, only if the list exists - 'LRANGE', // key start stop Get a range of elements from a list - 'LREM', // key count value Remove elements from a list - 'LSET', // key index value Set the value of an element in a list by its index - 'LTRIM', // key start stop Trim a list to the specified range - 'MGET', // key [key ...] Get the values of all the given keys - 'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one. - 'MONITOR', // Listen for all requests received by the server in real time - 'MOVE', // key db Move a key to another database - 'MSET', // key value [key value ...] Set multiple keys to multiple values - 'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist - 'MULTI', // Mark the start of a transaction block - 'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects - 'PERSIST', // key Remove the expiration from a key - 'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds - 'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds - 'PING', // Ping the server - 'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key - 'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns - 'PTTL', // key Get the time to live for a key in milliseconds - 'PUBLISH', // channel message Post a message to a channel - 'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns - 'QUIT', // Close the connection - 'RANDOMKEY', // Return a random key from the keyspace - 'RENAME', // key newkey Rename a key - 'RENAMENX', // key newkey Rename a key, only if the new key does not exist - 'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP. - 'RPOP', // key Remove and get the last element in a list - 'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it - 'RPUSH', // key value [value ...] Append one or multiple values to a list - 'RPUSHX', // key value Append a value to a list, only if the list exists - 'SADD', // key member [member ...] Add one or more members to a set - 'SAVE', // Synchronously save the dataset to disk - 'SCARD', // key Get the number of members in a set - 'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache. - 'SCRIPT FLUSH', // Remove all the scripts from the script cache. - 'SCRIPT KILL', // Kill the script currently in execution. - 'SCRIPT LOAD', // script Load the specified Lua script into the script cache. - 'SDIFF', // key [key ...] Subtract multiple sets - 'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key - 'SELECT', // index Change the selected database for the current connection - 'SET', // key value Set the string value of a key - 'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key - 'SETEX', // key seconds value Set the value and expiration of a key - 'SETNX', // key value Set the value of a key, only if the key does not exist - 'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset - 'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server - 'SINTER', // key [key ...] Intersect multiple sets - 'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key - 'SISMEMBER', // key member Determine if a given value is a member of a set - 'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master - 'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log - 'SMEMBERS', // key Get all the members in a set - 'SMOVE', // source destination member Move a member from one set to another - 'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set - 'SPOP', // key Remove and return a random member from a set - 'SRANDMEMBER', // key [count] Get one or multiple random members from a set - 'SREM', // key member [member ...] Remove one or more members from a set - 'STRLEN', // key Get the length of the value stored in a key - 'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels - 'SUNION', // key [key ...] Add multiple sets - 'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key - 'SYNC', // Internal command used for replication - 'TIME', // Return the current server time - 'TTL', // key Get the time to live for a key - 'TYPE', // key Determine the type stored at key - 'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels - 'UNWATCH', // Forget about all watched keys - 'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block - 'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists - 'ZCARD', // key Get the number of members in a sorted set - 'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values - 'ZINCRBY', // key increment member Increment the score of a member in a sorted set - 'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key - 'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index - 'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score - 'ZRANK', // key member Determine the index of a member in a sorted set - 'ZREM', // key member [member ...] Remove one or more members from a sorted set - 'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes - 'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores - 'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low - 'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low - '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 - ]; - /** - * @var resource redis socket connection - */ - private $_socket; - - /** - * Closes the connection when this component is being serialized. - * @return array - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } - - /** - * Returns a value indicating whether the DB connection is established. - * @return boolean whether the DB connection is established - */ - public function getIsActive() - { - return $this->_socket !== null; - } - - /** - * Establishes a DB connection. - * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - if ($this->_socket === null) { - if (empty($this->dsn)) { - throw new InvalidConfigException('Connection.dsn cannot be empty.'); - } - $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, - $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)); - } - if ($this->password !== null) { - $this->executeCommand('AUTH', [$this->password]); - } - $this->executeCommand('SELECT', [$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, $errorDescription, (int)$errorNumber); - } - } - } - - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - if ($this->_socket !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); - $this->executeCommand('QUIT'); - stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); - $this->_socket = null; - } - } - - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->trigger(self::EVENT_AFTER_OPEN); - } - - /** - * Returns the name of the DB driver for the current [[dsn]]. - * @return string name of the DB driver - */ - public function getDriverName() - { - if (($pos = strpos($this->dsn, ':')) !== false) { - return strtolower(substr($this->dsn, 0, $pos)); - } else { - return 'redis'; - } - } - - /** - * @return LuaScriptBuilder - */ - public function getLuaScriptBuilder() - { - return new LuaScriptBuilder(); - } - - /** - * - * @param string $name - * @param array $params - * @return mixed - */ - public function __call($name, $params) - { - $redisCommand = strtoupper(Inflector::camel2words($name, false)); - if (in_array($redisCommand, $this->redisCommands)) { - return $this->executeCommand($name, $params); - } else { - return parent::__call($name, $params); - } - } - - /** - * Executes a redis command. - * For a list of available commands and their parameters see http://redis.io/commands. - * - * @param string $name the name of the command - * @param array $params list of parameters for the command - * @return array|bool|null|string Dependend on the executed command this method - * will return different data types: - * - * - `true` for commands that return "status reply". - * - `string` for commands that return "integer reply" - * as the value is in the range of a signed 64 bit integer. - * - `string` or `null` for commands that return "bulk reply". - * - `array` for commands that return "Multi-bulk replies". - * - * See [redis protocol description](http://redis.io/topics/protocol) - * for details on the mentioned reply types. - * @trows Exception for commands that return [error reply](http://redis.io/topics/protocol#error-reply). - */ - public function executeCommand($name, $params=[]) - { - $this->open(); - - array_unshift($params, $name); - $command = '*' . count($params) . "\r\n"; - foreach($params as $arg) { - $command .= '$' . mb_strlen($arg, '8bit') . "\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) { - 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 - return true; - 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 = fread($this->_socket, $line + 2)) === 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); - } - } -} diff --git a/framework/yii/redis/LuaScriptBuilder.php b/framework/yii/redis/LuaScriptBuilder.php deleted file mode 100644 index 81dff3f..0000000 --- a/framework/yii/redis/LuaScriptBuilder.php +++ /dev/null @@ -1,365 +0,0 @@ - - * @since 2.0 - */ -class LuaScriptBuilder extends \yii\base\Object -{ - /** - * Builds a Lua script for finding a list of records - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildAll($query) - { - // TODO add support for orderBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks'); - } - - /** - * Builds a Lua script for finding one record - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildOne($query) - { - // TODO add support for orderBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks'); - } - - /** - * Builds a Lua script for finding a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildColumn($query, $column) - { - // TODO add support for orderBy and indexBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks'); - } - - /** - * Builds a Lua script for getting count of records - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildCount($query) - { - return $this->build($query, 'n=n+1', 'n'); - } - - /** - * Builds a Lua script for finding the sum of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildSum($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n'); - } - - /** - * Builds a Lua script for finding the average of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildAverage($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n'); - } - - /** - * Builds a Lua script for finding the min value of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildMin($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); - return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v'); - } - - /** - * @param ActiveQuery $query the query used to build the script - * @param string $buildResult the lua script for building the result - * @param string $return the lua variable that should be returned - * @return string - */ - private function build($query, $buildResult, $return) - { - if (!empty($query->orderBy)) { - throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); - } - - $columns = []; - if ($query->where !== null) { - $condition = $this->buildCondition($query->where, $columns); - } else { - $condition = 'true'; - } - - $start = $query->offset === null ? 0 : $query->offset; - $limitCondition = 'i>' . $start . ($query->limit === null ? '' : ' and i<=' . ($start + $query->limit)); - - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName()); - $loadColumnValues = ''; - foreach($columns as $column => $alias) { - $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, '$column')\n"; - } - - return << 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - ]; - - if (!is_array($condition)) { - throw new NotSupportedException('Where condition must be an array in redis ActiveRecord.'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtolower($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition, $columns); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition, $columns); - } - } - - private function buildHashCondition($condition, &$columns) - { - $parts = []; - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', [$column, $value], $columns); - } else { - $column = $this->addColumn($column, $columns); - if ($value === null) { - $parts[] = "$column==nil"; - } elseif ($value instanceof Expression) { - $parts[] = "$column==" . $value->expression; - } else { - $value = $this->quoteValue($value); - $parts[] = "$column==$value"; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') and (', $parts) . ')'; - } - - private function buildAndCondition($operator, $operands, &$columns) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $columns); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - private function buildBetweenCondition($operator, $operands, &$columns) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - $value1 = $this->quoteValue($value1); - $value2 = $this->quoteValue($value2); - $column = $this->addColumn($column, $columns); - return "$column >= $value1 and $column <= $value2"; - } - - private function buildInCondition($operator, $operands, &$columns) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'in' ? 'false' : 'true'; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values, $columns); - } elseif (is_array($column)) { - $column = reset($column); - } - $columnAlias = $this->addColumn($column, $columns); - $parts = []; - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $parts[] = "$columnAlias==nil"; - } elseif ($value instanceof Expression) { - $parts[] = "$columnAlias==" . $value->expression; - } else { - $value = $this->quoteValue($value); - $parts[] = "$columnAlias==$value"; - } - } - $operator = $operator === 'in' ? '' : 'not '; - return "$operator(" . implode(' or ', $parts) . ')'; - } - - protected function buildCompositeInCondition($operator, $inColumns, $values, &$columns) - { - $vss = []; - foreach ($values as $value) { - $vs = []; - foreach ($inColumns as $column) { - $column = $this->addColumn($column, $columns); - if (isset($value[$column])) { - $vs[] = "$column==" . $this->quoteValue($value[$column]); - } else { - $vs[] = "$column==nil"; - } - } - $vss[] = '(' . implode(' and ', $vs) . ')'; - } - $operator = $operator === 'in' ? '' : 'not '; - return "$operator(" . implode(' or ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands, &$columns) - { - throw new NotSupportedException('LIKE conditions are not suppoerted by redis ActiveRecord.'); - } -} diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index 13a1026..28e5abe 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -30,7 +30,9 @@ return [ 'fixture' => __DIR__ . '/postgres.sql', ], 'redis' => [ - 'dsn' => 'redis://localhost:6379/0', + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, 'password' => null, ], ], diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php new file mode 100644 index 0000000..74dd49e --- /dev/null +++ b/tests/unit/extensions/redis/ActiveRecordTest.php @@ -0,0 +1,465 @@ +getConnection(); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); + $customer->save(false); + +// INSERT INTO tbl_category (name) VALUES ('Books'); +// INSERT INTO tbl_category (name) VALUES ('Movies'); + + $item = new Item(); + $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(['customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + } + + public function testFind() + { + // find one + $result = Customer::find(); + $this->assertTrue($result instanceof ActiveQuery); + $customer = $result->one(); + $this->assertTrue($customer instanceof Customer); + + // find all + $customers = Customer::find()->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[0] instanceof Customer); + $this->assertTrue($customers[1] instanceof Customer); + $this->assertTrue($customers[2] instanceof Customer); + + // find by a single primary key + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer = Customer::find(5); + $this->assertNull($customer); + $customer = Customer::find(['id' => [5, 6, 1]]); + $this->assertEquals(1, count($customer)); + $customer = Customer::find()->where(['id' => [5, 6, 1]])->one(); + $this->assertNotNull($customer); + + // query scalar + $customerName = Customer::find()->where(['id' => 2])->scalar('name'); + $this->assertEquals('user2', $customerName); + + // find by column values + $customer = Customer::find(['id' => 2, 'name' => 'user2']); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer = Customer::find(['id' => 2, 'name' => 'user1']); + $this->assertNull($customer); + $customer = Customer::find(['id' => 5]); + $this->assertNull($customer); + + // find by attributes + $customer = Customer::find()->where(['name' => 'user2'])->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(2, $customer->id); + + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); + $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(2, Customer::find()->average('id')); + $this->assertEquals(1, Customer::find()->min('id')); + $this->assertEquals(3, Customer::find()->max('id')); + + // scope + $this->assertEquals(2, Customer::find()->active()->count()); + + // asArray + $customer = Customer::find()->where(['id' => 2])->asArray()->one(); + $this->assertEquals(array( + 'id' => '2', + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => '1', + ), $customer); + + // indexBy + $customers = Customer::find()->indexBy('name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['user1'] instanceof Customer); + $this->assertTrue($customers['user2'] instanceof Customer); + $this->assertTrue($customers['user3'] instanceof Customer); + + // indexBy callable + $customers = Customer::find()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; +// })->orderBy('id')->all(); + })->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['1-user1'] instanceof Customer); + $this->assertTrue($customers['2-user2'] instanceof Customer); + $this->assertTrue($customers['3-user3'] instanceof Customer); + } + + public function testFindCount() + { + $this->assertEquals(3, Customer::find()->count()); + $this->assertEquals(1, Customer::find()->limit(1)->count()); + $this->assertEquals(2, Customer::find()->limit(2)->count()); + $this->assertEquals(1, Customer::find()->offset(2)->limit(2)->count()); + } + + public function testFindLimit() + { + // all() + $customers = Customer::find()->all(); + $this->assertEquals(3, count($customers)); + + $customers = Customer::find()->limit(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user1', $customers[0]->name); + + $customers = Customer::find()->limit(1)->offset(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + + $customers = Customer::find()->limit(1)->offset(2)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user3', $customers[0]->name); + + $customers = Customer::find()->limit(2)->offset(1)->all(); + $this->assertEquals(2, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + $this->assertEquals('user3', $customers[1]->name); + + $customers = Customer::find()->limit(2)->offset(3)->all(); + $this->assertEquals(0, count($customers)); + + // one() + $customer = Customer::find()->one(); + $this->assertEquals('user1', $customer->name); + + $customer = Customer::find()->offset(0)->one(); + $this->assertEquals('user1', $customer->name); + + $customer = Customer::find()->offset(1)->one(); + $this->assertEquals('user2', $customer->name); + + $customer = Customer::find()->offset(2)->one(); + $this->assertEquals('user3', $customer->name); + + $customer = Customer::find()->offset(3)->one(); + $this->assertNull($customer); + + } + + public function testFindComplexCondition() + { + $this->assertEquals(2, Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->count()); + $this->assertEquals(2, count(Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->all())); + + $this->assertEquals(2, Customer::find()->where(['id' => [1,2]])->count()); + $this->assertEquals(2, count(Customer::find()->where(['id' => [1,2]])->all())); + + $this->assertEquals(1, Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->count()); + $this->assertEquals(1, count(Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->all())); + } + + public function testSum() + { + $this->assertEquals(6, OrderItem::find()->count()); + $this->assertEquals(7, OrderItem::find()->sum('quantity')); + } + + public function testFindColumn() + { + $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); +// TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name')); + } + + public function testExists() + { + $this->assertTrue(Customer::find()->where(['id' => 2])->exists()); + $this->assertFalse(Customer::find()->where(['id' => 5])->exists()); + } + + public function testFindLazy() + { + /** @var $customer Customer */ + $customer = Customer::find(2); + $orders = $customer->orders; + $this->assertEquals(2, count($orders)); + + $orders = $customer->getOrders()->where(['id' => 3])->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(3, $orders[0]->id); + } + + public function testFindEager() + { + $customers = Customer::find()->with('orders')->all(); + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + } + + public function testFindLazyVia() + { + /** @var $order Order */ + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(1); + $order->id = 100; + $this->assertEquals([], $order->items); + } + + public function testFindEagerViaRelation() + { + $orders = Order::find()->with('items')->all(); + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindNestedRelation() + { + $customers = Customer::find()->with('orders', 'orders.items')->all(); + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + $this->assertEquals(0, count($customers[2]->orders)); + $this->assertEquals(2, count($customers[0]->orders[0]->items)); + $this->assertEquals(3, count($customers[1]->orders[0]->items)); + $this->assertEquals(1, count($customers[1]->orders[1]->items)); + } + + public function testLink() + { + $customer = Customer::find(2); + $this->assertEquals(2, count($customer->orders)); + + // has many + $order = new Order; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer->link('orders', $order); + $this->assertEquals(3, count($customer->orders)); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(3, count($customer->getOrders()->all())); + $this->assertEquals(2, $order->customer_id); + + // belongs to + $order = new Order; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer = Customer::find(1); + $this->assertNull($order->customer); + $order->link('customer', $customer); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(1, $order->customer_id); + $this->assertEquals(1, $order->customer->id); + + // via model + $order = Order::find(1); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]); + $this->assertNull($orderItem); + $item = Item::find(3); + $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]); + $this->assertTrue($orderItem instanceof OrderItem); + $this->assertEquals(10, $orderItem->quantity); + $this->assertEquals(100, $orderItem->subtotal); + } + + public function testUnlink() + { + // has many + $customer = Customer::find(2); + $this->assertEquals(2, count($customer->orders)); + $customer->unlink('orders', $customer->orders[1], true); + $this->assertEquals(1, count($customer->orders)); + $this->assertNull(Order::find(3)); + + // via model + $order = Order::find(2); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $order->unlink('items', $order->items[2], true); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + } + + public function testInsert() + { + $customer = new Customer; + $customer->email = 'user4@example.com'; + $customer->name = 'user4'; + $customer->address = 'address4'; + + $this->assertNull($customer->id); + $this->assertTrue($customer->isNewRecord); + + $customer->save(); + + $this->assertEquals(4, $customer->id); + $this->assertFalse($customer->isNewRecord); + } + + // TODO test serial column incr + + public function testUpdate() + { + // save + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + $customer->name = 'user2x'; + $customer->save(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $customer2 = Customer::find(2); + $this->assertEquals('user2x', $customer2->name); + + // updateAll + $customer = Customer::find(3); + $this->assertEquals('user3', $customer->name); + $ret = Customer::updateAll(array( + 'name' => 'temp', + ), ['id' => 3]); + $this->assertEquals(1, $ret); + $customer = Customer::find(3); + $this->assertEquals('temp', $customer->name); + } + + public function testUpdateCounters() + { + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + $orderItem = OrderItem::find($pk); + $this->assertEquals(1, $orderItem->quantity); + $ret = $orderItem->updateCounters(['quantity' => -1]); + $this->assertTrue($ret); + $this->assertEquals(0, $orderItem->quantity); + $orderItem = OrderItem::find($pk); + $this->assertEquals(0, $orderItem->quantity); + + // updateAllCounters + $pk = ['order_id' => 1, 'item_id' => 2]; + $orderItem = OrderItem::find($pk); + $this->assertEquals(2, $orderItem->quantity); + $ret = OrderItem::updateAllCounters(array( + 'quantity' => 3, + 'subtotal' => -10, + ), $pk); + $this->assertEquals(1, $ret); + $orderItem = OrderItem::find($pk); + $this->assertEquals(5, $orderItem->quantity); + $this->assertEquals(30, $orderItem->subtotal); + } + + public function testUpdatePk() + { + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + $orderItem = OrderItem::find($pk); + $this->assertEquals(2, $orderItem->order_id); + $this->assertEquals(4, $orderItem->item_id); + + $orderItem->order_id = 2; + $orderItem->item_id = 10; + $orderItem->save(); + + $this->assertNull(OrderItem::find($pk)); + $this->assertNotNull(OrderItem::find(['order_id' => 2, 'item_id' => 10])); + } + + public function testDelete() + { + // delete + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer->delete(); + $customer = Customer::find(2); + $this->assertNull($customer); + + // deleteAll + $customers = Customer::find()->all(); + $this->assertEquals(2, count($customers)); + $ret = Customer::deleteAll(); + $this->assertEquals(2, $ret); + $customers = Customer::find()->all(); + $this->assertEquals(0, count($customers)); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/redis/RedisCacheTest.php b/tests/unit/extensions/redis/RedisCacheTest.php new file mode 100644 index 0000000..a235bb5 --- /dev/null +++ b/tests/unit/extensions/redis/RedisCacheTest.php @@ -0,0 +1,101 @@ +getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + } + $connection = new Connection($params); + if(!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new Cache(); + } + return $this->_cacheInstance; + } + + public function testExpireMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_test_ms')); + } + + public function testExpireAddMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_testa_ms')); + } + + /** + * Store a value that is 2 times buffer size big + * https://github.com/yiisoft/yii2/issues/743 + */ + public function testLargeData() + { + $cache = $this->getCacheInstance(); + + $data=str_repeat('XX',8192); // http://www.php.net/manual/en/function.fread.php + $key='bigdata1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + + // try with multibyte string + $data=str_repeat('ЖЫ',8192); // http://www.php.net/manual/en/function.fread.php + $key='bigdata2'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + } + + public function testMultiByteGetAndSet() + { + $cache = $this->getCacheInstance(); + + $data=['abc'=>'ежик',2=>'def']; + $key='data1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key,$data); + $this->assertTrue($cache->get($key)===$data); + } + +} \ No newline at end of file diff --git a/tests/unit/extensions/redis/RedisConnectionTest.php b/tests/unit/extensions/redis/RedisConnectionTest.php new file mode 100644 index 0000000..7a6bd91 --- /dev/null +++ b/tests/unit/extensions/redis/RedisConnectionTest.php @@ -0,0 +1,58 @@ +getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->set('YIITESTKEY', 'YIITESTVALUE'); + $db->close(); + + $db = $this->getConnection(false); + $db->database = 0; + $db->open(); + $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); + $db->close(); + + $db = $this->getConnection(false); + $db->database = 1; + $db->open(); + $this->assertNull($db->get('YIITESTKEY')); + $db->close(); + } + + public function keyValueData() + { + return array( + array(123), + array(-123), + array(0), + array('test'), + array("test\r\ntest"), + array(''), + ); + } + + /** + * @dataProvider keyValueData + */ + public function testStoreGet($data) + { + $db = $this->getConnection(true); + + $db->set('hi', $data); + $this->assertEquals($data, $db->get('hi')); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/redis/RedisTestCase.php b/tests/unit/extensions/redis/RedisTestCase.php new file mode 100644 index 0000000..ed4dccd --- /dev/null +++ b/tests/unit/extensions/redis/RedisTestCase.php @@ -0,0 +1,48 @@ +getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + } + $connection = new Connection($params); + if(!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + parent::setUp(); + } + + /** + * @param bool $reset whether to clean up the test database + * @return Connection + */ + public function getConnection($reset = true) + { + $databases = $this->getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : []; + $db = new Connection($params); + if ($reset) { + $db->open(); + $db->flushdb(); + } + return $db; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/RedisCacheTest.php b/tests/unit/framework/caching/RedisCacheTest.php deleted file mode 100644 index 3201a49..0000000 --- a/tests/unit/framework/caching/RedisCacheTest.php +++ /dev/null @@ -1,95 +0,0 @@ - 'localhost', - 'port' => 6379, - 'database' => 0, - 'dataTimeout' => 0.1, - ]; - $dsn = $config['hostname'] . ':' .$config['port']; - if (!@stream_socket_client($dsn, $errorNumber, $errorDescription, 0.5)) { - $this->markTestSkipped('No redis server running at ' . $dsn .' : ' . $errorNumber . ' - ' . $errorDescription); - } - - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new RedisCache($config); - } - return $this->_cacheInstance; - } - - public function testExpireMilliseconds() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); - usleep(100000); - $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); - usleep(300000); - $this->assertFalse($cache->get('expire_test_ms')); - } - - public function testExpireAddMilliseconds() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); - usleep(100000); - $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); - usleep(300000); - $this->assertFalse($cache->get('expire_testa_ms')); - } - - /** - * Store a value that is 2 times buffer size big - * https://github.com/yiisoft/yii2/issues/743 - */ - public function testLargeData() - { - $cache = $this->getCacheInstance(); - - $data=str_repeat('XX',8192); // http://www.php.net/manual/en/function.fread.php - $key='bigdata1'; - - $this->assertFalse($cache->get($key)); - $cache->set($key,$data); - $this->assertTrue($cache->get($key)===$data); - - // try with multibyte string - $data=str_repeat('ЖЫ',8192); // http://www.php.net/manual/en/function.fread.php - $key='bigdata2'; - - $this->assertFalse($cache->get($key)); - $cache->set($key,$data); - $this->assertTrue($cache->get($key)===$data); - } - - public function testMultiByteGetAndSet() - { - $cache = $this->getCacheInstance(); - - $data=['abc'=>'ежик',2=>'def']; - $key='data1'; - - $this->assertFalse($cache->get($key)); - $cache->set($key,$data); - $this->assertTrue($cache->get($key)===$data); - } - -} \ No newline at end of file diff --git a/tests/unit/framework/redis/ActiveRecordTest.php b/tests/unit/framework/redis/ActiveRecordTest.php deleted file mode 100644 index 31907f7..0000000 --- a/tests/unit/framework/redis/ActiveRecordTest.php +++ /dev/null @@ -1,466 +0,0 @@ -getConnection(); - - $customer = new Customer(); - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); - $customer->save(false); - -// INSERT INTO tbl_category (name) VALUES ('Books'); -// INSERT INTO tbl_category (name) VALUES ('Movies'); - - $item = new Item(); - $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); - $item->save(false); - - $order = new Order(); - $order->setAttributes(['customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0], false); - $order->save(false); - $order = new Order(); - $order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false); - $order->save(false); - $order = new Order(); - $order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], false); - $order->save(false); - - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); - $orderItem->save(false); - } - - public function testFind() - { - // find one - $result = Customer::find(); - $this->assertTrue($result instanceof ActiveQuery); - $customer = $result->one(); - $this->assertTrue($customer instanceof Customer); - - // find all - $customers = Customer::find()->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers[0] instanceof Customer); - $this->assertTrue($customers[1] instanceof Customer); - $this->assertTrue($customers[2] instanceof Customer); - - // find by a single primary key - $customer = Customer::find(2); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $customer = Customer::find(5); - $this->assertNull($customer); - $customer = Customer::find(['id' => [5, 6, 1]]); - $this->assertEquals(1, count($customer)); - $customer = Customer::find()->where(['id' => [5, 6, 1]])->one(); - $this->assertNotNull($customer); - - // query scalar - $customerName = Customer::find()->where(['id' => 2])->scalar('name'); - $this->assertEquals('user2', $customerName); - - // find by column values - $customer = Customer::find(['id' => 2, 'name' => 'user2']); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $customer = Customer::find(['id' => 2, 'name' => 'user1']); - $this->assertNull($customer); - $customer = Customer::find(['id' => 5]); - $this->assertNull($customer); - - // find by attributes - $customer = Customer::find()->where(['name' => 'user2'])->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(2, $customer->id); - - // find count, sum, average, min, max, scalar - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(6, Customer::find()->sum('id')); - $this->assertEquals(2, Customer::find()->average('id')); - $this->assertEquals(1, Customer::find()->min('id')); - $this->assertEquals(3, Customer::find()->max('id')); - - // scope - $this->assertEquals(2, Customer::find()->active()->count()); - - // asArray - $customer = Customer::find()->where(['id' => 2])->asArray()->one(); - $this->assertEquals(array( - 'id' => '2', - 'email' => 'user2@example.com', - 'name' => 'user2', - 'address' => 'address2', - 'status' => '1', - ), $customer); - - // indexBy - $customers = Customer::find()->indexBy('name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['user1'] instanceof Customer); - $this->assertTrue($customers['user2'] instanceof Customer); - $this->assertTrue($customers['user3'] instanceof Customer); - - // indexBy callable - $customers = Customer::find()->indexBy(function ($customer) { - return $customer->id . '-' . $customer->name; -// })->orderBy('id')->all(); - })->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['1-user1'] instanceof Customer); - $this->assertTrue($customers['2-user2'] instanceof Customer); - $this->assertTrue($customers['3-user3'] instanceof Customer); - } - - public function testFindCount() - { - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(1, Customer::find()->limit(1)->count()); - $this->assertEquals(2, Customer::find()->limit(2)->count()); - $this->assertEquals(1, Customer::find()->offset(2)->limit(2)->count()); - } - - public function testFindLimit() - { - // all() - $customers = Customer::find()->all(); - $this->assertEquals(3, count($customers)); - - $customers = Customer::find()->limit(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user1', $customers[0]->name); - - $customers = Customer::find()->limit(1)->offset(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - - $customers = Customer::find()->limit(1)->offset(2)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user3', $customers[0]->name); - - $customers = Customer::find()->limit(2)->offset(1)->all(); - $this->assertEquals(2, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - $this->assertEquals('user3', $customers[1]->name); - - $customers = Customer::find()->limit(2)->offset(3)->all(); - $this->assertEquals(0, count($customers)); - - // one() - $customer = Customer::find()->one(); - $this->assertEquals('user1', $customer->name); - - $customer = Customer::find()->offset(0)->one(); - $this->assertEquals('user1', $customer->name); - - $customer = Customer::find()->offset(1)->one(); - $this->assertEquals('user2', $customer->name); - - $customer = Customer::find()->offset(2)->one(); - $this->assertEquals('user3', $customer->name); - - $customer = Customer::find()->offset(3)->one(); - $this->assertNull($customer); - - } - - public function testFindComplexCondition() - { - $this->assertEquals(2, Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->count()); - $this->assertEquals(2, count(Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->all())); - - $this->assertEquals(2, Customer::find()->where(['id' => [1,2]])->count()); - $this->assertEquals(2, count(Customer::find()->where(['id' => [1,2]])->all())); - - $this->assertEquals(1, Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->count()); - $this->assertEquals(1, count(Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->all())); - } - - public function testSum() - { - $this->assertEquals(6, OrderItem::find()->count()); - $this->assertEquals(7, OrderItem::find()->sum('quantity')); - } - - public function testFindColumn() - { - $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); -// TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name')); - } - - public function testExists() - { - $this->assertTrue(Customer::find()->where(['id' => 2])->exists()); - $this->assertFalse(Customer::find()->where(['id' => 5])->exists()); - } - - public function testFindLazy() - { - /** @var $customer Customer */ - $customer = Customer::find(2); - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - - $orders = $customer->getOrders()->where(['id' => 3])->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(3, $orders[0]->id); - } - - public function testFindEager() - { - $customers = Customer::find()->with('orders')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - } - - public function testFindLazyVia() - { - /** @var $order Order */ - $order = Order::find(1); - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - - $order = Order::find(1); - $order->id = 100; - $this->assertEquals([], $order->items); - } - - public function testFindEagerViaRelation() - { - $orders = Order::find()->with('items')->all(); - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testFindNestedRelation() - { - $customers = Customer::find()->with('orders', 'orders.items')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - $this->assertEquals(0, count($customers[2]->orders)); - $this->assertEquals(2, count($customers[0]->orders[0]->items)); - $this->assertEquals(3, count($customers[1]->orders[0]->items)); - $this->assertEquals(1, count($customers[1]->orders[1]->items)); - } - - public function testLink() - { - $customer = Customer::find(2); - $this->assertEquals(2, count($customer->orders)); - - // has many - $order = new Order; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer->link('orders', $order); - $this->assertEquals(3, count($customer->orders)); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(3, count($customer->getOrders()->all())); - $this->assertEquals(2, $order->customer_id); - - // belongs to - $order = new Order; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer = Customer::find(1); - $this->assertNull($order->customer); - $order->link('customer', $customer); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(1, $order->customer_id); - $this->assertEquals(1, $order->customer->id); - - // via model - $order = Order::find(1); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]); - $this->assertNull($orderItem); - $item = Item::find(3); - $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]); - $this->assertTrue($orderItem instanceof OrderItem); - $this->assertEquals(10, $orderItem->quantity); - $this->assertEquals(100, $orderItem->subtotal); - } - - public function testUnlink() - { - // has many - $customer = Customer::find(2); - $this->assertEquals(2, count($customer->orders)); - $customer->unlink('orders', $customer->orders[1], true); - $this->assertEquals(1, count($customer->orders)); - $this->assertNull(Order::find(3)); - - // via model - $order = Order::find(2); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $order->unlink('items', $order->items[2], true); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - } - - public function testInsert() - { - $customer = new Customer; - $customer->email = 'user4@example.com'; - $customer->name = 'user4'; - $customer->address = 'address4'; - - $this->assertNull($customer->id); - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - - $this->assertEquals(4, $customer->id); - $this->assertFalse($customer->isNewRecord); - } - - // TODO test serial column incr - - public function testUpdate() - { - // save - $customer = Customer::find(2); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $this->assertFalse($customer->isNewRecord); - $customer->name = 'user2x'; - $customer->save(); - $this->assertEquals('user2x', $customer->name); - $this->assertFalse($customer->isNewRecord); - $customer2 = Customer::find(2); - $this->assertEquals('user2x', $customer2->name); - - // updateAll - $customer = Customer::find(3); - $this->assertEquals('user3', $customer->name); - $ret = Customer::updateAll(array( - 'name' => 'temp', - ), ['id' => 3]); - $this->assertEquals(1, $ret); - $customer = Customer::find(3); - $this->assertEquals('temp', $customer->name); - } - - public function testUpdateCounters() - { - // updateCounters - $pk = ['order_id' => 2, 'item_id' => 4]; - $orderItem = OrderItem::find($pk); - $this->assertEquals(1, $orderItem->quantity); - $ret = $orderItem->updateCounters(['quantity' => -1]); - $this->assertTrue($ret); - $this->assertEquals(0, $orderItem->quantity); - $orderItem = OrderItem::find($pk); - $this->assertEquals(0, $orderItem->quantity); - - // updateAllCounters - $pk = ['order_id' => 1, 'item_id' => 2]; - $orderItem = OrderItem::find($pk); - $this->assertEquals(2, $orderItem->quantity); - $ret = OrderItem::updateAllCounters(array( - 'quantity' => 3, - 'subtotal' => -10, - ), $pk); - $this->assertEquals(1, $ret); - $orderItem = OrderItem::find($pk); - $this->assertEquals(5, $orderItem->quantity); - $this->assertEquals(30, $orderItem->subtotal); - } - - public function testUpdatePk() - { - // updateCounters - $pk = ['order_id' => 2, 'item_id' => 4]; - $orderItem = OrderItem::find($pk); - $this->assertEquals(2, $orderItem->order_id); - $this->assertEquals(4, $orderItem->item_id); - - $orderItem->order_id = 2; - $orderItem->item_id = 10; - $orderItem->save(); - - $this->assertNull(OrderItem::find($pk)); - $this->assertNotNull(OrderItem::find(['order_id' => 2, 'item_id' => 10])); - } - - public function testDelete() - { - // delete - $customer = Customer::find(2); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $customer->delete(); - $customer = Customer::find(2); - $this->assertNull($customer); - - // deleteAll - $customers = Customer::find()->all(); - $this->assertEquals(2, count($customers)); - $ret = Customer::deleteAll(); - $this->assertEquals(2, $ret); - $customers = Customer::find()->all(); - $this->assertEquals(0, count($customers)); - } -} \ No newline at end of file diff --git a/tests/unit/framework/redis/RedisConnectionTest.php b/tests/unit/framework/redis/RedisConnectionTest.php deleted file mode 100644 index af39e0e..0000000 --- a/tests/unit/framework/redis/RedisConnectionTest.php +++ /dev/null @@ -1,69 +0,0 @@ -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 keyValueData() - { - return array( - array(123), - array(-123), - array(0), - array('test'), - array("test\r\ntest"), - array(''), - ); - } - - /** - * @dataProvider keyValueData - */ - public function testStoreGet($data) - { - $db = $this->getConnection(true); - - $db->set('hi', $data); - $this->assertEquals($data, $db->get('hi')); - } -} \ No newline at end of file diff --git a/tests/unit/framework/redis/RedisTestCase.php b/tests/unit/framework/redis/RedisTestCase.php deleted file mode 100644 index 12e539d..0000000 --- a/tests/unit/framework/redis/RedisTestCase.php +++ /dev/null @@ -1,51 +0,0 @@ -mockApplication(); - - $databases = $this->getParam('databases'); - $params = isset($databases['redis']) ? $databases['redis'] : null; - 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 - */ - public function getConnection($reset = true) - { - $databases = $this->getParam('databases'); - $params = isset($databases['redis']) ? $databases['redis'] : array(); - $db = new Connection; - $db->dsn = $params['dsn']; - $db->password = $params['password']; - if ($reset) { - $db->open(); - $db->flushall(); - } - return $db; - } -} \ No newline at end of file