From 563171eba42eb6681ace2732f54fbf5db097dd79 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Sep 2013 17:09:40 +0200 Subject: [PATCH] moved redis out of yii\db namespace --- framework/yii/db/redis/ActiveQuery.php | 374 -------------- framework/yii/db/redis/ActiveRecord.php | 572 --------------------- framework/yii/db/redis/ActiveRelation.php | 249 --------- framework/yii/db/redis/Connection.php | 425 --------------- framework/yii/db/redis/RecordSchema.php | 53 -- framework/yii/db/redis/Transaction.php | 91 ---- framework/yii/db/redis/schema.md | 35 -- framework/yii/redis/ActiveQuery.php | 374 ++++++++++++++ framework/yii/redis/ActiveRecord.php | 572 +++++++++++++++++++++ framework/yii/redis/ActiveRelation.php | 247 +++++++++ framework/yii/redis/RecordSchema.php | 53 ++ tests/unit/data/ar/redis/ActiveRecord.php | 4 +- tests/unit/data/ar/redis/Customer.php | 4 +- tests/unit/data/ar/redis/Item.php | 2 +- tests/unit/data/ar/redis/Order.php | 2 +- tests/unit/data/ar/redis/OrderItem.php | 2 +- tests/unit/framework/db/redis/ActiveRecordTest.php | 422 --------------- .../framework/db/redis/RedisConnectionTest.php | 66 --- tests/unit/framework/db/redis/RedisTestCase.php | 57 -- tests/unit/framework/redis/ActiveRecordTest.php | 422 +++++++++++++++ tests/unit/framework/redis/RedisConnectionTest.php | 66 +++ tests/unit/framework/redis/RedisTestCase.php | 51 ++ 22 files changed, 1792 insertions(+), 2351 deletions(-) delete mode 100644 framework/yii/db/redis/ActiveQuery.php delete mode 100644 framework/yii/db/redis/ActiveRecord.php delete mode 100644 framework/yii/db/redis/ActiveRelation.php delete mode 100644 framework/yii/db/redis/Connection.php delete mode 100644 framework/yii/db/redis/RecordSchema.php delete mode 100644 framework/yii/db/redis/Transaction.php delete mode 100644 framework/yii/db/redis/schema.md create mode 100644 framework/yii/redis/ActiveQuery.php create mode 100644 framework/yii/redis/ActiveRecord.php create mode 100644 framework/yii/redis/ActiveRelation.php create mode 100644 framework/yii/redis/RecordSchema.php delete mode 100644 tests/unit/framework/db/redis/ActiveRecordTest.php delete mode 100644 tests/unit/framework/db/redis/RedisConnectionTest.php delete mode 100644 tests/unit/framework/db/redis/RedisTestCase.php create mode 100644 tests/unit/framework/redis/ActiveRecordTest.php create mode 100644 tests/unit/framework/redis/RedisConnectionTest.php create mode 100644 tests/unit/framework/redis/RedisTestCase.php diff --git a/framework/yii/db/redis/ActiveQuery.php b/framework/yii/db/redis/ActiveQuery.php deleted file mode 100644 index 1d44d97..0000000 --- a/framework/yii/db/redis/ActiveQuery.php +++ /dev/null @@ -1,374 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\db\redis; - -/** - * ActiveQuery represents a DB query associated with an Active Record class. - * - * ActiveQuery instances are usually created by [[yii\db\redis\ActiveRecord::find()]] - * and [[yii\db\redis\ActiveRecord::count()]]. - * - * ActiveQuery mainly provides the following methods to retrieve the query results: - * - * - [[one()]]: returns a single record populated with the first row of data. - * - [[all()]]: returns all records based on the query results. - * - [[count()]]: returns the number of records. - * - [[sum()]]: returns the sum over the specified column. - * - [[average()]]: returns the average over the specified column. - * - [[min()]]: returns the min over the specified column. - * - [[max()]]: returns the max over the specified column. - * - [[scalar()]]: returns the value of the first column in the first row of the query result. - * - [[exists()]]: returns a value indicating whether the query result has data or not. - * - * You can use query methods, such as [[limit()]], [[orderBy()]] to customize the query options. - * - * ActiveQuery also provides the following additional query options: - * - * - [[with()]]: list of relations that this query should be performed with. - * - [[indexBy()]]: the name of the column by which the query result should be indexed. - * - [[asArray()]]: whether to return each record as an array. - * - * These options can be configured using methods of the same name. For example: - * - * ~~~ - * $customers = Customer::find()->with('orders')->asArray()->all(); - * ~~~ - * - * @author Carsten Brandt - * @since 2.0 - */ -class ActiveQuery extends \yii\base\Component -{ - /** - * @var string the name of the ActiveRecord class. - */ - public $modelClass; - /** - * @var array list of relations that this query should be performed with - */ - public $with; - /** - * @var string the name of the column by which query results should be indexed by. - * This is only used when the query result is returned as an array when calling [[all()]]. - */ - public $indexBy; - /** - * @var boolean whether to return each record as an array. If false (default), an object - * of [[modelClass]] will be created to represent each record. - */ - public $asArray; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. - * If not set, it means starting from the beginning. - * If less than zero it means starting n elements from the end. - */ - public $offset; - /** - * @var array array of primary keys of the records to find. - */ - public $primaryKeys; - - /** - * List of multiple pks must be zero based - * - * @param $primaryKeys - * @return ActiveQuery - */ - public function primaryKeys($primaryKeys) { - if (is_array($primaryKeys) && isset($primaryKeys[0])) { - $this->primaryKeys = $primaryKeys; - } else { - $this->primaryKeys = array($primaryKeys); - } - - return $this; - } - - /** - * Executes query and returns all results as an array. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all() - { - $modelClass = $this->modelClass; - /** @var Connection $db */ - $db = $modelClass::getDb(); - if (($primaryKeys = $this->primaryKeys) === null) { - $start = $this->offset === null ? 0 : $this->offset; - $end = $this->limit === null ? -1 : $start + $this->limit; - $primaryKeys = $db->executeCommand('LRANGE', array($modelClass::tableName(), $start, $end)); - } - $rows = array(); - foreach($primaryKeys as $pk) { - $key = $modelClass::tableName() . ':a:' . $modelClass::hashPk($pk); - // get attributes - $data = $db->executeCommand('HGETALL', array($key)); - $row = array(); - for($i=0;$icreateModels($rows); - if (!empty($this->with)) { - $this->populateRelations($models, $this->with); - } - return $models; - } else { - return array(); - } - } - - /** - * Executes query and returns a single row of result. - * @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() - { - $modelClass = $this->modelClass; - /** @var Connection $db */ - $db = $modelClass::getDb(); - if (($primaryKeys = $this->primaryKeys) === null) { - $start = $this->offset === null ? 0 : $this->offset; - $primaryKeys = $db->executeCommand('LRANGE', array($modelClass::tableName(), $start, $start + 1)); - } - $pk = reset($primaryKeys); - $key = $modelClass::tableName() . ':a:' . $modelClass::hashPk($pk); - // get attributes - $data = $db->executeCommand('HGETALL', array($key)); - if ($data === array()) { - return null; - } - $row = array(); - for($i=0;$iasArray) { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - $model = $class::create($row); - if (!empty($this->with)) { - $models = array($model); - $this->populateRelations($models, $this->with); - $model = $models[0]; - } - return $model; - } else { - return $row; - } - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * Make sure you properly quote column names. - * @return integer number of records - */ - public function count() - { - $modelClass = $this->modelClass; - /** @var Connection $db */ - $db = $modelClass::getDb(); - return $db->executeCommand('LLEN', array($modelClass::tableName())); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the first column in the first row of the query results. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if the query result is empty. - */ - public function scalar($column) - { - $record = $this->one(); - return $record->$column; - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @return boolean whether the query result contains any row of data. - */ - public function exists() - { - return $this->one() !== null; - } - - - /** - * Sets the [[asArray]] property. - * TODO: refactor, it is duplicated from yii/db/ActiveQuery - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return ActiveQuery the query object itself - */ - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - /** - * Sets the LIMIT part of the query. - * TODO: refactor, it is duplicated from yii/db/Query - * @param integer $limit the limit - * @return Query the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * TODO: refactor, it is duplicated from yii/db/Query - * @param integer $offset the offset - * @return Query the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } - - /** - * Specifies the relations with which this query should be performed. - * - * The parameters to this method can be either one or multiple strings, or a single array - * of relation names and the optional callbacks to customize the relations. - * - * The followings are some usage examples: - * - * ~~~ - * // find customers together with their orders and country - * Customer::find()->with('orders', 'country')->all(); - * // find customers together with their country and orders of status 1 - * Customer::find()->with(array( - * 'orders' => function($query) { - * $query->andWhere('status = 1'); - * }, - * 'country', - * ))->all(); - * ~~~ - * - * TODO: refactor, it is duplicated from yii/db/ActiveQuery - * @return ActiveQuery the query object itself - */ - public function with() - { - $this->with = func_get_args(); - if (isset($this->with[0]) && is_array($this->with[0])) { - // the parameter is given as an array - $this->with = $this->with[0]; - } - return $this; - } - - /** - * Sets the [[indexBy]] property. - * TODO: refactor, it is duplicated from yii/db/ActiveQuery - * @param string $column the name of the column by which the query results should be indexed by. - * @return ActiveQuery the query object itself - */ - public function indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - // TODO: refactor, it is duplicated from yii/db/ActiveQuery - private function createModels($rows) - { - $models = array(); - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - foreach ($rows as $row) { - $models[$row[$this->indexBy]] = $row; - } - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $models[] = $class::create($row); - } - } else { - foreach ($rows as $row) { - $model = $class::create($row); - $models[$model->{$this->indexBy}] = $model; - } - } - } - return $models; - } - - // TODO: refactor, it is duplicated from yii/db/ActiveQuery - private function populateRelations(&$models, $with) - { - $primaryModel = new $this->modelClass; - $relations = $this->normalizeRelations($primaryModel, $with); - foreach ($relations as $name => $relation) { - if ($relation->asArray === null) { - // inherit asArray from primary query - $relation->asArray = $this->asArray; - } - $relation->findWith($name, $models); - } - } - - /** - * TODO: refactor, it is duplicated from yii/db/ActiveQuery - * @param ActiveRecord $model - * @param array $with - * @return ActiveRelation[] - */ - private function normalizeRelations($model, $with) - { - $relations = array(); - foreach ($with as $name => $callback) { - if (is_integer($name)) { - $name = $callback; - $callback = null; - } - if (($pos = strpos($name, '.')) !== false) { - // with sub-relations - $childName = substr($name, $pos + 1); - $name = substr($name, 0, $pos); - } else { - $childName = null; - } - - $t = strtolower($name); - if (!isset($relations[$t])) { - $relation = $model->getRelation($name); - $relation->primaryModel = null; - $relations[$t] = $relation; - } else { - $relation = $relations[$t]; - } - - if (isset($childName)) { - $relation->with[$childName] = $callback; - } elseif ($callback !== null) { - call_user_func($callback, $relation); - } - } - return $relations; - } -} diff --git a/framework/yii/db/redis/ActiveRecord.php b/framework/yii/db/redis/ActiveRecord.php deleted file mode 100644 index b043e21..0000000 --- a/framework/yii/db/redis/ActiveRecord.php +++ /dev/null @@ -1,572 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\db\redis; - -use yii\base\InvalidCallException; -use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; -use yii\base\NotSupportedException; -use yii\base\UnknownMethodException; -use yii\db\TableSchema; - -/** - * ActiveRecord is the base class for classes representing relational data in terms of objects. - * - * - * - * @author Carsten Brandt - * @since 2.0 - */ -abstract 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->redis; - } - - /** - * Creates an [[ActiveQuery]] instance for query purpose. - * - * @include @yii/db/ActiveRecord-find.md - * - * @param mixed $q the query parameter. This can be one of the followings: - * - * - a scalar value (integer or string): query by a single primary key value and return the - * corresponding record. - * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. - * - null: return a new [[ActiveQuery]] object for further query purpose. - * - * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance - * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be - * returned (null will be returned if there is no matching). - * @see createQuery() - */ - public static function find($q = null) // TODO optimize API - { - $query = static::createQuery(); - if (is_array($q)) { - return $query->primaryKeys($q)->one(); - } elseif ($q !== null) { - // query by primary key - $primaryKey = static::primaryKey(); - return $query->primaryKeys(array($primaryKey[0] => $q))->one(); - } - return $query; - } - - public static function hashPk($pk) - { - return (is_array($pk) ? implode('-', $pk) : $pk); // TODO escape PK glue - } - - /** - * @inheritdoc - */ - public static function findBySql($sql, $params = array()) - { - throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord'); - } - - /** - * Creates an [[ActiveQuery]] instance. - * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery() - { - return new ActiveQuery(array( - 'modelClass' => get_called_class(), - )); - } - - /** - * Declares the name of the database table associated with this AR class. - * @return string the table name - */ - public static function tableName() - { - return static::getTableSchema()->name; - } - - /** - * Returns the schema information of the DB table associated with this AR class. - * @return TableSchema the schema information of the DB table associated with this AR class. - */ - public static function getTableSchema() - { - // TODO should be cached - throw new InvalidConfigException(__CLASS__.'::getTableSchema() needs to be overridden in subclasses and return a TableSchema.'); - } - - /** - * Inserts a row into the associated database table using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[changedAttributes|changed attribute values]] will be inserted into database. - * - * If the table's primary key is auto-incremental and is null during insertion, - * it will be populated with the actual value after insertion. - * - * For example, to insert a customer record: - * - * ~~~ - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - */ - 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 = array(); -// if ($values === array()) { - foreach ($this->primaryKey() as $key) { - $pk[$key] = $values[$key] = $this->getAttribute($key); - if ($pk[$key] === null) { - $pk[$key] = $values[$key] = $db->executeCommand('INCR', array(static::tableName() . ':s:' . $key)); - $this->setAttribute($key, $values[$key]); - } - } -// } - // save pk in a findall pool - $db->executeCommand('RPUSH', array(static::tableName(), static::hashPk($pk))); - - $key = static::tableName() . ':a:' . static::hashPk($pk); - // save attributes - $args = array($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(array('status' => 1), 'status = 2'); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = '', $params = array()) - { - $db = static::getDb(); - if ($condition==='') { - $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); - } - if (empty($attributes)) { - return 0; - } - $n=0; - foreach($condition as $pk) { - $key = static::tableName() . ':a:' . static::hashPk($pk); - // save attributes - $args = array($key); - foreach($attributes as $attribute => $value) { - $args[] = $attribute; - $args[] = $value; - } - $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(array('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 string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = '', $params = array()) - { - if (is_array($condition) && !isset($condition[0])) { // TODO do this in all *All methods - $condition = array($condition); - } - $db = static::getDb(); - if ($condition==='') { - $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); - } - $n=0; - foreach($condition as $pk) { // TODO allow multiple pks as condition - $key = static::tableName() . ':a:' . static::hashPk($pk); - foreach($counters as $attribute => $value) { - $db->executeCommand('HINCRBY', array($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 string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = '', $params = array()) - { - $db = static::getDb(); - if ($condition==='') { - $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); - } - if (empty($condition)) { - return 0; - } - $attributeKeys = array(); - foreach($condition as $pk) { - $pk = static::hashPk($pk); - $db->executeCommand('LREM', array(static::tableName(), 0, $pk)); - $attributeKeys[] = static::tableName() . ':a:' . $pk; - } - return $db->executeCommand('DEL', $attributeKeys);// TODO make this atomic or document as NOT - } - - /** - * Declares a `has-one` relation. - * The declaration is returned in terms of an [[ActiveRelation]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-one` relation means that there is at most one related record matching - * the criteria set by this relation, e.g., a customer has one country. - * - * For example, to declare the `country` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getCountry() - * { - * return $this->hasOne('Country', array('id' => 'country_id')); - * } - * ~~~ - * - * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name - * in the related class `Country`, while the 'country_id' value refers to an attribute name - * in the current AR class. - * - * Call methods declared in [[ActiveRelation]] to further customize the relation. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the columns in the table associated with the `$class` model, while the values of the - * array refer to the corresponding columns in the table associated with this AR class. - * @return ActiveRelation the relation object. - */ - public function hasOne($class, $link) - { - return new ActiveRelation(array( - 'modelClass' => $this->getNamespacedClass($class), - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => false, - )); - } - - /** - * Declares a `has-many` relation. - * The declaration is returned in terms of an [[ActiveRelation]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-many` relation means that there are multiple related records matching - * the criteria set by this relation, e.g., a customer has many orders. - * - * For example, to declare the `orders` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getOrders() - * { - * return $this->hasMany('Order', array('customer_id' => 'id')); - * } - * ~~~ - * - * Note that in the above, the 'customer_id' key in the `$link` parameter refers to - * an attribute name in the related class `Order`, while the 'id' value refers to - * an attribute name in the current AR class. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the columns in the table associated with the `$class` model, while the values of the - * array refer to the corresponding columns in the table associated with this AR class. - * @return ActiveRelation the relation object. - */ - public function hasMany($class, $link) - { - return new ActiveRelation(array( - 'modelClass' => $this->getNamespacedClass($class), - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => true, - )); - } - - /** - * Returns the relation object with the specified name. - * A relation is defined by a getter method which returns an [[ActiveRelation]] object. - * It can be declared in either the Active Record class itself or one of its behaviors. - * @param string $name the relation name - * @return ActiveRelation the relation object - * @throws InvalidParamException if the named relation does not exist. - */ - public function getRelation($name) - { - $getter = 'get' . $name; - try { - $relation = $this->$getter(); - if ($relation instanceof ActiveRelation) { - return $relation; - } - } catch (UnknownMethodException $e) { - } - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); - } - - /** - * Establishes the relationship between two models. - * - * The relationship is established by setting the foreign key value(s) in one model - * to be the corresponding primary key value(s) in the other model. - * The model with the foreign key will be saved into database without performing validation. - * - * If the relationship involves a pivot table, a new row will be inserted into the - * pivot table which contains the primary key values from both models. - * - * Note that this method requires that the primary key value is not null. - * - * @param string $name the name of the relationship - * @param ActiveRecord $model the model to be linked with the current one. - * @param array $extraColumns additional column values to be saved into the pivot table. - * This parameter is only meaningful for a relationship involving a pivot table - * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.) - * @throws InvalidCallException if the method is unable to link two models. - */ - public function link($name, $model, $extraColumns = array()) - { - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - // TODO - - - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2) { - if ($this->getIsNewRecord() && $model->getIsNewRecord()) { - throw new InvalidCallException('Unable to link models: both models are newly created.'); - } elseif ($this->getIsNewRecord()) { - $this->bindModels(array_flip($relation->link), $this, $model); - } else { - $this->bindModels($relation->link, $model, $this); - } - } elseif ($p1) { - $this->bindModels(array_flip($relation->link), $this, $model); - } elseif ($p2) { - $this->bindModels($relation->link, $model, $this); - } else { - throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); - } - } - - // update lazily loaded related objects - if (!$relation->multiple) { - $this->_related[$name] = $model; - } elseif (isset($this->_related[$name])) { - if ($relation->indexBy !== null) { - $indexBy = $relation->indexBy; - $this->_related[$name][$model->$indexBy] = $model; - } else { - $this->_related[$name][] = $model; - } - } - } - - /** - * @param array $link - * @param ActiveRecord $foreignModel - * @param ActiveRecord $primaryModel - * @throws InvalidCallException - */ - private function bindModels($link, $foreignModel, $primaryModel) - { - foreach ($link as $fk => $pk) { - $value = $primaryModel->$pk; - if ($value === null) { - throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); - } - $foreignModel->$fk = $value; - } - $foreignModel->save(false); - } - - /** - * Destroys the relationship between two models. - * - * The model with the foreign key of the relationship will be deleted if `$delete` is true. - * Otherwise, the foreign key will be set null and the model will be saved without validation. - * - * @param string $name the name of the relationship. - * @param ActiveRecord $model the model to be unlinked from the current one. - * @param boolean $delete whether to delete the model that contains the foreign key. - * If false, the model's foreign key will be set null and saved. - * If true, the model containing the foreign key will be deleted. - * @throws InvalidCallException if the models cannot be unlinked - */ - public function unlink($name, $model, $delete = false) - { - // TODO - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - if (is_array($relation->via)) { - /** @var $viaRelation ActiveRelation */ - list($viaName, $viaRelation) = $relation->via; - /** @var $viaClass ActiveRecord */ - $viaClass = $viaRelation->modelClass; - $viaTable = $viaClass::tableName(); - unset($this->_related[strtolower($viaName)]); - } else { - $viaRelation = $relation->via; - $viaTable = reset($relation->via->from); - } - $columns = array(); - foreach ($viaRelation->link as $a => $b) { - $columns[$a] = $this->$b; - } - foreach ($relation->link as $a => $b) { - $columns[$b] = $model->$a; - } - $command = static::getDb()->createCommand(); - if ($delete) { - $command->delete($viaTable, $columns)->execute(); - } else { - $nulls = array(); - foreach (array_keys($columns) as $a) { - $nulls[$a] = null; - } - $command->update($viaTable, $nulls, $columns)->execute(); - } - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2 || $p2) { - foreach ($relation->link as $a => $b) { - $model->$a = null; - } - $delete ? $model->delete() : $model->save(false); - } elseif ($p1) { - foreach ($relation->link as $b) { - $this->$b = null; - } - $delete ? $this->delete() : $this->save(false); - } else { - throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); - } - } - - if (!$relation->multiple) { - unset($this->_related[$name]); - } elseif (isset($this->_related[$name])) { - /** @var $b ActiveRecord */ - foreach ($this->_related[$name] as $a => $b) { - if ($model->getPrimaryKey() == $b->getPrimaryKey()) { - unset($this->_related[$name][$a]); - } - } - } - } - - /** - * TODO duplicate code, refactor - * @param array $keys - * @return boolean - */ - private function isPrimaryKey($keys) - { - $pks = $this->primaryKey(); - foreach ($keys as $key) { - if (!in_array($key, $pks, true)) { - return false; - } - } - return true; - } - - - - // TODO implement link and unlink -} diff --git a/framework/yii/db/redis/ActiveRelation.php b/framework/yii/db/redis/ActiveRelation.php deleted file mode 100644 index e01f3a4..0000000 --- a/framework/yii/db/redis/ActiveRelation.php +++ /dev/null @@ -1,249 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\db\redis; - -use yii\base\NotSupportedException; - -/** - * ActiveRecord is the base class for classes representing relational data in terms of objects. - * - * - * @author Carsten Brandt - * @since 2.0 - */ -class ActiveRelation extends \yii\db\redis\ActiveQuery -{ - /** - * @var boolean whether this relation should populate all query results into AR instances. - * If false, only the first row of the results will be retrieved. - */ - public $multiple; - /** - * @var ActiveRecord the primary model that this relation is associated with. - * This is used only in lazy loading with dynamic query options. - */ - public $primaryModel; - /** - * @var array the columns of the primary and foreign tables that establish the relation. - * The array keys must be columns of the table for this relation, and the array values - * must be the corresponding columns from the primary table. - * Do not prefix or quote the column names as this will be done automatically by Yii. - */ - public $link; - /** - * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] - * or [[viaTable()]] to set this property instead of directly setting it. - */ - public $via; - - /** - * Specifies the relation associated with the pivot table. - * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. - * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return ActiveRelation the relation object itself. - */ - public function via($relationName, $callable = null) - { - $relation = $this->primaryModel->getRelation($relationName); - $this->via = array($relationName, $relation); - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - } - - /** - * Specifies the pivot table. - * @param string $tableName the name of the pivot table. - * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. - * The keys of the array represent the columns in the pivot table, and the values represent the columns - * in the [[primaryModel]] table. - * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. - * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return ActiveRelation - * / - public function viaTable($tableName, $link, $callable = null) - { - $relation = new ActiveRelation(array( - 'modelClass' => get_class($this->primaryModel), - 'from' => array($tableName), - 'link' => $link, - 'multiple' => true, - 'asArray' => true, - )); - $this->via = $relation; - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - }*/ - - /** - * Finds the related records and populates them into the primary models. - * This method is internally by [[ActiveQuery]]. Do not call it directly. - * @param string $name the relation name - * @param array $primaryModels primary models - * @return array the related models - * @throws InvalidConfigException - */ - public function findWith($name, &$primaryModels) - { - if (!is_array($this->link)) { - throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); - } - - if ($this->via instanceof self) { - // TODO - // via pivot table - /** @var $viaQuery ActiveRelation */ - $viaQuery = $this->via; - $viaModels = $viaQuery->findPivotRows($primaryModels); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // TODO - // via relation - /** @var $viaQuery ActiveRelation */ - list($viaName, $viaQuery) = $this->via; - $viaQuery->primaryModel = null; - $viaModels = $viaQuery->findWith($viaName, $primaryModels); - $this->filterByModels($viaModels); - } else { - $this->filterByModels($primaryModels); - } - - if (count($primaryModels) === 1 && !$this->multiple) { - $model = $this->one(); - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $model); - } else { - $primaryModels[$i][$name] = $model; - } - } - return array($model); - } else { - $models = $this->all(); - if (isset($viaModels, $viaQuery)) { - $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); - } else { - $buckets = $this->buildBuckets($models, $this->link); - } - - $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); - foreach ($primaryModels as $i => $primaryModel) { - $key = $this->getModelKey($primaryModel, $link); - $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? array() : null); - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $value); - } else { - $primaryModels[$i][$name] = $value; - } - } - return $models; - } - } - - /** - * @param array $models - * @param array $link - * @param array $viaModels - * @param array $viaLink - * @return array - */ - private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) - { - $buckets = array(); - $linkKeys = array_keys($link); - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if ($this->indexBy !== null) { - $buckets[$key][$i] = $model; - } else { - $buckets[$key][] = $model; - } - } - - if ($viaModels !== null) { - $viaBuckets = array(); - $viaLinkKeys = array_keys($viaLink); - $linkValues = array_values($link); - foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, $viaLinkKeys); - $key2 = $this->getModelKey($viaModel, $linkValues); - if (isset($buckets[$key2])) { - foreach ($buckets[$key2] as $i => $bucket) { - if ($this->indexBy !== null) { - $viaBuckets[$key1][$i] = $bucket; - } else { - $viaBuckets[$key1][] = $bucket; - } - } - } - } - $buckets = $viaBuckets; - } - - if (!$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } - } - return $buckets; - } - - /** - * @param ActiveRecord|array $model - * @param array $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - if (count($attributes) > 1) { - $key = array(); - foreach ($attributes as $attribute) { - $key[] = $model[$attribute]; - } - return serialize($key); - } else { - $attribute = reset($attributes); - return $model[$attribute]; - } - } - - - /** - * @param array $models - */ - private function filterByModels($models) - { - $attributes = array_keys($this->link); - $values = array(); - if (count($attributes) ===1) { - // single key - $attribute = reset($this->link); - foreach ($models as $model) { - $values[] = $model[$attribute]; - } - } else { - // composite keys - foreach ($models as $model) { - $v = array(); - foreach ($this->link as $attribute => $link) { - $v[$attribute] = $model[$link]; - } - $values[] = $v; - } - } - $this->primaryKeys($values); - } - -} diff --git a/framework/yii/db/redis/Connection.php b/framework/yii/db/redis/Connection.php deleted file mode 100644 index 46db575..0000000 --- a/framework/yii/db/redis/Connection.php +++ /dev/null @@ -1,425 +0,0 @@ -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', array($this->password)); - } - $this->executeCommand('SELECT', array($db)); - $this->initConnection(); - } else { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); - $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; - throw new Exception($message, $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; - $this->_transaction = 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 currently active transaction. - * @return Transaction the currently active transaction. Null if no active transaction. - */ - public function getTransaction() - { - return $this->_transaction && $this->_transaction->isActive ? $this->_transaction : null; - } - - /** - * Starts a transaction. - * @return Transaction the transaction initiated - */ - public function beginTransaction() - { - $this->open(); - $this->_transaction = new Transaction(array( - 'db' => $this, - )); - $this->_transaction->begin(); - return $this->_transaction; - } - - /** - * 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'; - } - } - - /** - * - * @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=array()) - { - $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 = array(); - 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/db/redis/RecordSchema.php b/framework/yii/db/redis/RecordSchema.php deleted file mode 100644 index 3bc219d..0000000 --- a/framework/yii/db/redis/RecordSchema.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ - -namespace yii\db\redis; - - -use yii\base\InvalidConfigException; -use yii\db\TableSchema; - -/** - * Class RecordSchema defines the data schema for a redis active record - * - * As there is no schema in a redis DB this class is used to define one. - * - * @package yii\db\redis - */ -class RecordSchema extends TableSchema -{ - /** - * @var string[] column names. - */ - public $columns = array(); - - /** - * @return string the column type - */ - public function getColumn($name) - { - parent::getColumn($name); - } - - public function init() - { - if (empty($this->name)) { - throw new InvalidConfigException('name of RecordSchema must not be empty.'); - } - if (empty($this->primaryKey)) { - throw new InvalidConfigException('primaryKey of RecordSchema must not be empty.'); - } - if (!is_array($this->primaryKey)) { - $this->primaryKey = array($this->primaryKey); - } - foreach($this->primaryKey as $pk) { - if (!isset($this->columns[$pk])) { - throw new InvalidConfigException('primaryKey '.$pk.' is not a colum of RecordSchema.'); - } - } - } -} \ No newline at end of file diff --git a/framework/yii/db/redis/Transaction.php b/framework/yii/db/redis/Transaction.php deleted file mode 100644 index 721a7be..0000000 --- a/framework/yii/db/redis/Transaction.php +++ /dev/null @@ -1,91 +0,0 @@ -_active; - } - - /** - * Begins a transaction. - * @throws InvalidConfigException if [[connection]] is null - */ - public function begin() - { - if (!$this->_active) { - if ($this->db === null) { - throw new InvalidConfigException('Transaction::db must be set.'); - } - \Yii::trace('Starting transaction', __CLASS__); - $this->db->open(); - $this->db->createCommand('MULTI')->execute(); - $this->_active = true; - } - } - - /** - * Commits a transaction. - * @throws Exception if the transaction or the DB connection is not active. - */ - public function commit() - { - if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Committing transaction', __CLASS__); - $this->db->createCommand('EXEC')->execute(); - // TODO handle result of EXEC - $this->_active = false; - } else { - throw new Exception('Failed to commit transaction: transaction was inactive.'); - } - } - - /** - * Rolls back a transaction. - * @throws Exception if the transaction or the DB connection is not active. - */ - public function rollback() - { - if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Rolling back transaction', __CLASS__); - $this->db->pdo->commit(); - $this->_active = false; - } else { - throw new Exception('Failed to roll back transaction: transaction was inactive.'); - } - } -} diff --git a/framework/yii/db/redis/schema.md b/framework/yii/db/redis/schema.md deleted file mode 100644 index 1bd45b3..0000000 --- a/framework/yii/db/redis/schema.md +++ /dev/null @@ -1,35 +0,0 @@ -To allow AR to be stored in redis we need a special Schema for it. - -HSET prefix:className:primaryKey - - -http://redis.io/commands - -Current Redis connection: -https://github.com/jamm/Memory - - -# Queries - -wrap all these in transactions MULTI - -## insert - -SET all attribute key-value pairs -SET all relation key-value pairs -make sure to create back-relations - -## update - -SET all attribute key-value pairs -SET all relation key-value pairs - - -## delete - -DEL all attribute key-value pairs -DEL all relation key-value pairs -make sure to update back-relations - - -http://redis.io/commands/hmget sounds suiteable! \ No newline at end of file diff --git a/framework/yii/redis/ActiveQuery.php b/framework/yii/redis/ActiveQuery.php new file mode 100644 index 0000000..54b7d31 --- /dev/null +++ b/framework/yii/redis/ActiveQuery.php @@ -0,0 +1,374 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\redis; + +/** + * ActiveQuery represents a DB query associated with an Active Record class. + * + * ActiveQuery instances are usually created by [[yii\db\redis\ActiveRecord::find()]] + * and [[yii\db\redis\ActiveRecord::count()]]. + * + * ActiveQuery mainly provides the following methods to retrieve the query results: + * + * - [[one()]]: returns a single record populated with the first row of data. + * - [[all()]]: returns all records based on the query results. + * - [[count()]]: returns the number of records. + * - [[sum()]]: returns the sum over the specified column. + * - [[average()]]: returns the average over the specified column. + * - [[min()]]: returns the min over the specified column. + * - [[max()]]: returns the max over the specified column. + * - [[scalar()]]: returns the value of the first column in the first row of the query result. + * - [[exists()]]: returns a value indicating whether the query result has data or not. + * + * You can use query methods, such as [[limit()]], [[orderBy()]] to customize the query options. + * + * ActiveQuery also provides the following additional query options: + * + * - [[with()]]: list of relations that this query should be performed with. + * - [[indexBy()]]: the name of the column by which the query result should be indexed. + * - [[asArray()]]: whether to return each record as an array. + * + * These options can be configured using methods of the same name. For example: + * + * ~~~ + * $customers = Customer::find()->with('orders')->asArray()->all(); + * ~~~ + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveQuery extends \yii\base\Component +{ + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var array list of relations that this query should be performed with + */ + public $with; + /** + * @var string the name of the column by which query results should be indexed by. + * This is only used when the query result is returned as an array when calling [[all()]]. + */ + public $indexBy; + /** + * @var boolean whether to return each record as an array. If false (default), an object + * of [[modelClass]] will be created to represent each record. + */ + public $asArray; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. + * If not set, it means starting from the beginning. + * If less than zero it means starting n elements from the end. + */ + public $offset; + /** + * @var array array of primary keys of the records to find. + */ + public $primaryKeys; + + /** + * List of multiple pks must be zero based + * + * @param $primaryKeys + * @return ActiveQuery + */ + public function primaryKeys($primaryKeys) { + if (is_array($primaryKeys) && isset($primaryKeys[0])) { + $this->primaryKeys = $primaryKeys; + } else { + $this->primaryKeys = array($primaryKeys); + } + + return $this; + } + + /** + * Executes query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all() + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + if (($primaryKeys = $this->primaryKeys) === null) { + $start = $this->offset === null ? 0 : $this->offset; + $end = $this->limit === null ? -1 : $start + $this->limit; + $primaryKeys = $db->executeCommand('LRANGE', array($modelClass::tableName(), $start, $end)); + } + $rows = array(); + foreach($primaryKeys as $pk) { + $key = $modelClass::tableName() . ':a:' . $modelClass::hashPk($pk); + // get attributes + $data = $db->executeCommand('HGETALL', array($key)); + $row = array(); + for($i=0;$icreateModels($rows); + if (!empty($this->with)) { + $this->populateRelations($models, $this->with); + } + return $models; + } else { + return array(); + } + } + + /** + * Executes query and returns a single row of result. + * @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() + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + if (($primaryKeys = $this->primaryKeys) === null) { + $start = $this->offset === null ? 0 : $this->offset; + $primaryKeys = $db->executeCommand('LRANGE', array($modelClass::tableName(), $start, $start + 1)); + } + $pk = reset($primaryKeys); + $key = $modelClass::tableName() . ':a:' . $modelClass::hashPk($pk); + // get attributes + $data = $db->executeCommand('HGETALL', array($key)); + if ($data === array()) { + return null; + } + $row = array(); + for($i=0;$iasArray) { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::create($row); + if (!empty($this->with)) { + $models = array($model); + $this->populateRelations($models, $this->with); + $model = $models[0]; + } + return $model; + } else { + return $row; + } + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * Make sure you properly quote column names. + * @return integer number of records + */ + public function count() + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + return $db->executeCommand('LLEN', array($modelClass::tableName())); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar($column) + { + $record = $this->one(); + return $record->$column; + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @return boolean whether the query result contains any row of data. + */ + public function exists() + { + return $this->one() !== null; + } + + + /** + * Sets the [[asArray]] property. + * TODO: refactor, it is duplicated from yii/db/ActiveQuery + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return ActiveQuery the query object itself + */ + public function asArray($value = true) + { + $this->asArray = $value; + return $this; + } + + /** + * Sets the LIMIT part of the query. + * TODO: refactor, it is duplicated from yii/db/Query + * @param integer $limit the limit + * @return Query the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * TODO: refactor, it is duplicated from yii/db/Query + * @param integer $offset the offset + * @return Query the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } + + /** + * Specifies the relations with which this query should be performed. + * + * The parameters to this method can be either one or multiple strings, or a single array + * of relation names and the optional callbacks to customize the relations. + * + * The followings are some usage examples: + * + * ~~~ + * // find customers together with their orders and country + * Customer::find()->with('orders', 'country')->all(); + * // find customers together with their country and orders of status 1 + * Customer::find()->with(array( + * 'orders' => function($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ))->all(); + * ~~~ + * + * TODO: refactor, it is duplicated from yii/db/ActiveQuery + * @return ActiveQuery the query object itself + */ + public function with() + { + $this->with = func_get_args(); + if (isset($this->with[0]) && is_array($this->with[0])) { + // the parameter is given as an array + $this->with = $this->with[0]; + } + return $this; + } + + /** + * Sets the [[indexBy]] property. + * TODO: refactor, it is duplicated from yii/db/ActiveQuery + * @param string $column the name of the column by which the query results should be indexed by. + * @return ActiveQuery the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + // TODO: refactor, it is duplicated from yii/db/ActiveQuery + private function createModels($rows) + { + $models = array(); + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + foreach ($rows as $row) { + $models[$row[$this->indexBy]] = $row; + } + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $models[] = $class::create($row); + } + } else { + foreach ($rows as $row) { + $model = $class::create($row); + $models[$model->{$this->indexBy}] = $model; + } + } + } + return $models; + } + + // TODO: refactor, it is duplicated from yii/db/ActiveQuery + private function populateRelations(&$models, $with) + { + $primaryModel = new $this->modelClass; + $relations = $this->normalizeRelations($primaryModel, $with); + foreach ($relations as $name => $relation) { + if ($relation->asArray === null) { + // inherit asArray from primary query + $relation->asArray = $this->asArray; + } + $relation->findWith($name, $models); + } + } + + /** + * TODO: refactor, it is duplicated from yii/db/ActiveQuery + * @param ActiveRecord $model + * @param array $with + * @return ActiveRelation[] + */ + private function normalizeRelations($model, $with) + { + $relations = array(); + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + if (($pos = strpos($name, '.')) !== false) { + // with sub-relations + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + } else { + $childName = null; + } + + $t = strtolower($name); + if (!isset($relations[$t])) { + $relation = $model->getRelation($name); + $relation->primaryModel = null; + $relations[$t] = $relation; + } else { + $relation = $relations[$t]; + } + + if (isset($childName)) { + $relation->with[$childName] = $callback; + } elseif ($callback !== null) { + call_user_func($callback, $relation); + } + } + return $relations; + } +} diff --git a/framework/yii/redis/ActiveRecord.php b/framework/yii/redis/ActiveRecord.php new file mode 100644 index 0000000..44cd5d7 --- /dev/null +++ b/framework/yii/redis/ActiveRecord.php @@ -0,0 +1,572 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\redis; + +use yii\base\InvalidCallException; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\base\NotSupportedException; +use yii\base\UnknownMethodException; +use yii\db\TableSchema; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * + * + * @author Carsten Brandt + * @since 2.0 + */ +abstract 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->redis; + } + + /** + * Creates an [[ActiveQuery]] instance for query purpose. + * + * @include @yii/db/ActiveRecord-find.md + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. + * - null: return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + * @see createQuery() + */ + public static function find($q = null) // TODO optimize API + { + $query = static::createQuery(); + if (is_array($q)) { + return $query->primaryKeys($q)->one(); + } elseif ($q !== null) { + // query by primary key + $primaryKey = static::primaryKey(); + return $query->primaryKeys(array($primaryKey[0] => $q))->one(); + } + return $query; + } + + public static function hashPk($pk) + { + return is_array($pk) ? implode('-', $pk) : $pk; // TODO escape PK glue + } + + /** + * @inheritdoc + */ + public static function findBySql($sql, $params = array()) + { + throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord'); + } + + /** + * Creates an [[ActiveQuery]] instance. + * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery() + { + return new ActiveQuery(array( + 'modelClass' => get_called_class(), + )); + } + + /** + * Declares the name of the database table associated with this AR class. + * @return string the table name + */ + public static function tableName() + { + return static::getTableSchema()->name; + } + + /** + * Returns the schema information of the DB table associated with this AR class. + * @return TableSchema the schema information of the DB table associated with this AR class. + */ + public static function getTableSchema() + { + // TODO should be cached + throw new InvalidConfigException(__CLASS__.'::getTableSchema() needs to be overridden in subclasses and return a TableSchema.'); + } + + /** + * Inserts a row into the associated database table using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[changedAttributes|changed attribute values]] will be inserted into database. + * + * If the table's primary key is auto-incremental and is null during insertion, + * it will be populated with the actual value after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + 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 = array(); +// if ($values === array()) { + foreach ($this->primaryKey() as $key) { + $pk[$key] = $values[$key] = $this->getAttribute($key); + if ($pk[$key] === null) { + $pk[$key] = $values[$key] = $db->executeCommand('INCR', array(static::tableName() . ':s:' . $key)); + $this->setAttribute($key, $values[$key]); + } + } +// } + // save pk in a findall pool + $db->executeCommand('RPUSH', array(static::tableName(), static::hashPk($pk))); + + $key = static::tableName() . ':a:' . static::hashPk($pk); + // save attributes + $args = array($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(array('status' => 1), 'status = 2'); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = '', $params = array()) + { + $db = static::getDb(); + if ($condition==='') { + $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); + } + if (empty($attributes)) { + return 0; + } + $n=0; + foreach($condition as $pk) { + $key = static::tableName() . ':a:' . static::hashPk($pk); + // save attributes + $args = array($key); + foreach($attributes as $attribute => $value) { + $args[] = $attribute; + $args[] = $value; + } + $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(array('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 string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = '', $params = array()) + { + if (is_array($condition) && !isset($condition[0])) { // TODO do this in all *All methods + $condition = array($condition); + } + $db = static::getDb(); + if ($condition==='') { + $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); + } + $n=0; + foreach($condition as $pk) { // TODO allow multiple pks as condition + $key = static::tableName() . ':a:' . static::hashPk($pk); + foreach($counters as $attribute => $value) { + $db->executeCommand('HINCRBY', array($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 string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = '', $params = array()) + { + $db = static::getDb(); + if ($condition==='') { + $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); + } + if (empty($condition)) { + return 0; + } + $attributeKeys = array(); + foreach($condition as $pk) { + $pk = static::hashPk($pk); + $db->executeCommand('LREM', array(static::tableName(), 0, $pk)); + $attributeKeys[] = static::tableName() . ':a:' . $pk; + } + return $db->executeCommand('DEL', $attributeKeys);// TODO make this atomic or document as NOT + } + + /** + * Declares a `has-one` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-one` relation means that there is at most one related record matching + * the criteria set by this relation, e.g., a customer has one country. + * + * For example, to declare the `country` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getCountry() + * { + * return $this->hasOne('Country', array('id' => 'country_id')); + * } + * ~~~ + * + * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name + * in the related class `Country`, while the 'country_id' value refers to an attribute name + * in the current AR class. + * + * Call methods declared in [[ActiveRelation]] to further customize the relation. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the columns in the table associated with the `$class` model, while the values of the + * array refer to the corresponding columns in the table associated with this AR class. + * @return ActiveRelation the relation object. + */ + public function hasOne($class, $link) + { + return new ActiveRelation(array( + 'modelClass' => $this->getNamespacedClass($class), + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => false, + )); + } + + /** + * Declares a `has-many` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-many` relation means that there are multiple related records matching + * the criteria set by this relation, e.g., a customer has many orders. + * + * For example, to declare the `orders` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getOrders() + * { + * return $this->hasMany('Order', array('customer_id' => 'id')); + * } + * ~~~ + * + * Note that in the above, the 'customer_id' key in the `$link` parameter refers to + * an attribute name in the related class `Order`, while the 'id' value refers to + * an attribute name in the current AR class. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the columns in the table associated with the `$class` model, while the values of the + * array refer to the corresponding columns in the table associated with this AR class. + * @return ActiveRelation the relation object. + */ + public function hasMany($class, $link) + { + return new ActiveRelation(array( + 'modelClass' => $this->getNamespacedClass($class), + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => true, + )); + } + + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an [[ActiveRelation]] object. + * It can be declared in either the Active Record class itself or one of its behaviors. + * @param string $name the relation name + * @return ActiveRelation the relation object + * @throws InvalidParamException if the named relation does not exist. + */ + public function getRelation($name) + { + $getter = 'get' . $name; + try { + $relation = $this->$getter(); + if ($relation instanceof ActiveRelation) { + return $relation; + } + } catch (UnknownMethodException $e) { + } + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + } + + /** + * Establishes the relationship between two models. + * + * The relationship is established by setting the foreign key value(s) in one model + * to be the corresponding primary key value(s) in the other model. + * The model with the foreign key will be saved into database without performing validation. + * + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both models. + * + * Note that this method requires that the primary key value is not null. + * + * @param string $name the name of the relationship + * @param ActiveRecord $model the model to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.) + * @throws InvalidCallException if the method is unable to link two models. + */ + public function link($name, $model, $extraColumns = array()) + { + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + // TODO + + + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2) { + if ($this->getIsNewRecord() && $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models are newly created.'); + } elseif ($this->getIsNewRecord()) { + $this->bindModels(array_flip($relation->link), $this, $model); + } else { + $this->bindModels($relation->link, $model, $this); + } + } elseif ($p1) { + $this->bindModels(array_flip($relation->link), $this, $model); + } elseif ($p2) { + $this->bindModels($relation->link, $model, $this); + } else { + throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); + } + } + + // update lazily loaded related objects + if (!$relation->multiple) { + $this->_related[$name] = $model; + } elseif (isset($this->_related[$name])) { + if ($relation->indexBy !== null) { + $indexBy = $relation->indexBy; + $this->_related[$name][$model->$indexBy] = $model; + } else { + $this->_related[$name][] = $model; + } + } + } + + /** + * @param array $link + * @param ActiveRecord $foreignModel + * @param ActiveRecord $primaryModel + * @throws InvalidCallException + */ + private function bindModels($link, $foreignModel, $primaryModel) + { + foreach ($link as $fk => $pk) { + $value = $primaryModel->$pk; + if ($value === null) { + throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); + } + $foreignModel->$fk = $value; + } + $foreignModel->save(false); + } + + /** + * Destroys the relationship between two models. + * + * The model with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the model will be saved without validation. + * + * @param string $name the name of the relationship. + * @param ActiveRecord $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + * @throws InvalidCallException if the models cannot be unlinked + */ + public function unlink($name, $model, $delete = false) + { + // TODO + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if (is_array($relation->via)) { + /** @var $viaRelation ActiveRelation */ + list($viaName, $viaRelation) = $relation->via; + /** @var $viaClass ActiveRecord */ + $viaClass = $viaRelation->modelClass; + $viaTable = $viaClass::tableName(); + unset($this->_related[strtolower($viaName)]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = array(); + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + $command = static::getDb()->createCommand(); + if ($delete) { + $command->delete($viaTable, $columns)->execute(); + } else { + $nulls = array(); + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $command->update($viaTable, $nulls, $columns)->execute(); + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2 || $p2) { + foreach ($relation->link as $a => $b) { + $model->$a = null; + } + $delete ? $model->delete() : $model->save(false); + } elseif ($p1) { + foreach ($relation->link as $b) { + $this->$b = null; + } + $delete ? $this->delete() : $this->save(false); + } else { + throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); + } + } + + if (!$relation->multiple) { + unset($this->_related[$name]); + } elseif (isset($this->_related[$name])) { + /** @var $b ActiveRecord */ + foreach ($this->_related[$name] as $a => $b) { + if ($model->getPrimaryKey() == $b->getPrimaryKey()) { + unset($this->_related[$name][$a]); + } + } + } + } + + /** + * TODO duplicate code, refactor + * @param array $keys + * @return boolean + */ + private function isPrimaryKey($keys) + { + $pks = $this->primaryKey(); + foreach ($keys as $key) { + if (!in_array($key, $pks, true)) { + return false; + } + } + return true; + } + + + + // TODO implement link and unlink +} diff --git a/framework/yii/redis/ActiveRelation.php b/framework/yii/redis/ActiveRelation.php new file mode 100644 index 0000000..aae21fc --- /dev/null +++ b/framework/yii/redis/ActiveRelation.php @@ -0,0 +1,247 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\redis; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveRelation extends \yii\redis\ActiveQuery +{ + /** + * @var boolean whether this relation should populate all query results into AR instances. + * If false, only the first row of the results will be retrieved. + */ + public $multiple; + /** + * @var ActiveRecord the primary model that this relation is associated with. + * This is used only in lazy loading with dynamic query options. + */ + public $primaryModel; + /** + * @var array the columns of the primary and foreign tables that establish the relation. + * The array keys must be columns of the table for this relation, and the array values + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as this will be done automatically by Yii. + */ + public $link; + /** + * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] + * or [[viaTable()]] to set this property instead of directly setting it. + */ + public $via; + + /** + * Specifies the relation associated with the pivot table. + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return ActiveRelation the relation object itself. + */ + public function via($relationName, $callable = null) + { + $relation = $this->primaryModel->getRelation($relationName); + $this->via = array($relationName, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + } + + /** + * Specifies the pivot table. + * @param string $tableName the name of the pivot table. + * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. + * The keys of the array represent the columns in the pivot table, and the values represent the columns + * in the [[primaryModel]] table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return ActiveRelation + * / + public function viaTable($tableName, $link, $callable = null) + { + $relation = new ActiveRelation(array( + 'modelClass' => get_class($this->primaryModel), + 'from' => array($tableName), + 'link' => $link, + 'multiple' => true, + 'asArray' => true, + )); + $this->via = $relation; + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + }*/ + + /** + * Finds the related records and populates them into the primary models. + * This method is internally by [[ActiveQuery]]. Do not call it directly. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + * @throws InvalidConfigException + */ + public function findWith($name, &$primaryModels) + { + if (!is_array($this->link)) { + throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); + } + + if ($this->via instanceof self) { + // TODO + // via pivot table + /** @var $viaQuery ActiveRelation */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // TODO + // via relation + /** @var $viaQuery ActiveRelation */ + list($viaName, $viaQuery) = $this->via; + $viaQuery->primaryModel = null; + $viaModels = $viaQuery->findWith($viaName, $primaryModels); + $this->filterByModels($viaModels); + } else { + $this->filterByModels($primaryModels); + } + + if (count($primaryModels) === 1 && !$this->multiple) { + $model = $this->one(); + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $model); + } else { + $primaryModels[$i][$name] = $model; + } + } + return array($model); + } else { + $models = $this->all(); + if (isset($viaModels, $viaQuery)) { + $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); + } else { + $buckets = $this->buildBuckets($models, $this->link); + } + + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, $link); + $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? array() : null); + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $value); + } else { + $primaryModels[$i][$name] = $value; + } + } + return $models; + } + } + + /** + * @param array $models + * @param array $link + * @param array $viaModels + * @param array $viaLink + * @return array + */ + private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) + { + $buckets = array(); + $linkKeys = array_keys($link); + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + + if ($viaModels !== null) { + $viaBuckets = array(); + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); + foreach ($viaModels as $viaModel) { + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); + if (isset($buckets[$key2])) { + foreach ($buckets[$key2] as $i => $bucket) { + if ($this->indexBy !== null) { + $viaBuckets[$key1][$i] = $bucket; + } else { + $viaBuckets[$key1][] = $bucket; + } + } + } + } + $buckets = $viaBuckets; + } + + if (!$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + return $buckets; + } + + /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = array(); + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + return serialize($key); + } else { + $attribute = reset($attributes); + return $model[$attribute]; + } + } + + + /** + * @param array $models + */ + private function filterByModels($models) + { + $attributes = array_keys($this->link); + $values = array(); + if (count($attributes) ===1) { + // single key + $attribute = reset($this->link); + foreach ($models as $model) { + $values[] = $model[$attribute]; + } + } else { + // composite keys + foreach ($models as $model) { + $v = array(); + foreach ($this->link as $attribute => $link) { + $v[$attribute] = $model[$link]; + } + $values[] = $v; + } + } + $this->primaryKeys($values); + } + +} diff --git a/framework/yii/redis/RecordSchema.php b/framework/yii/redis/RecordSchema.php new file mode 100644 index 0000000..6c82515 --- /dev/null +++ b/framework/yii/redis/RecordSchema.php @@ -0,0 +1,53 @@ + + */ + +namespace yii\redis; + + +use yii\base\InvalidConfigException; +use yii\db\TableSchema; + +/** + * Class RecordSchema defines the data schema for a redis active record + * + * As there is no schema in a redis DB this class is used to define one. + * + * @package yii\db\redis + */ +class RecordSchema extends TableSchema +{ + /** + * @var string[] column names. + */ + public $columns = array(); + + /** + * @return string the column type + */ + public function getColumn($name) + { + parent::getColumn($name); + } + + public function init() + { + if (empty($this->name)) { + throw new InvalidConfigException('name of RecordSchema must not be empty.'); + } + if (empty($this->primaryKey)) { + throw new InvalidConfigException('primaryKey of RecordSchema must not be empty.'); + } + if (!is_array($this->primaryKey)) { + $this->primaryKey = array($this->primaryKey); + } + foreach($this->primaryKey as $pk) { + if (!isset($this->columns[$pk])) { + throw new InvalidConfigException('primaryKey '.$pk.' is not a colum of RecordSchema.'); + } + } + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/redis/ActiveRecord.php b/tests/unit/data/ar/redis/ActiveRecord.php index 7419479..9f6d526 100644 --- a/tests/unit/data/ar/redis/ActiveRecord.php +++ b/tests/unit/data/ar/redis/ActiveRecord.php @@ -7,7 +7,7 @@ namespace yiiunit\data\ar\redis; -use yii\db\redis\Connection; +use yii\redis\Connection; /** * ActiveRecord is ... @@ -15,7 +15,7 @@ use yii\db\redis\Connection; * @author Qiang Xue * @since 2.0 */ -class ActiveRecord extends \yii\db\redis\ActiveRecord +class ActiveRecord extends \yii\redis\ActiveRecord { public static $db; diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php index 91a75ff..30146b0 100644 --- a/tests/unit/data/ar/redis/Customer.php +++ b/tests/unit/data/ar/redis/Customer.php @@ -2,7 +2,7 @@ namespace yiiunit\data\ar\redis; -use yii\db\redis\RecordSchema; +use yii\redis\RecordSchema; class Customer extends ActiveRecord { @@ -12,7 +12,7 @@ class Customer extends ActiveRecord public $status2; /** - * @return \yii\db\redis\ActiveRelation + * @return \yii\redis\ActiveRelation */ public function getOrders() { diff --git a/tests/unit/data/ar/redis/Item.php b/tests/unit/data/ar/redis/Item.php index 55d1420..3ab0f10 100644 --- a/tests/unit/data/ar/redis/Item.php +++ b/tests/unit/data/ar/redis/Item.php @@ -2,7 +2,7 @@ namespace yiiunit\data\ar\redis; -use yii\db\redis\RecordSchema; +use yii\redis\RecordSchema; class Item extends ActiveRecord { diff --git a/tests/unit/data/ar/redis/Order.php b/tests/unit/data/ar/redis/Order.php index 8ccb12e..39979fe 100644 --- a/tests/unit/data/ar/redis/Order.php +++ b/tests/unit/data/ar/redis/Order.php @@ -2,7 +2,7 @@ namespace yiiunit\data\ar\redis; -use yii\db\redis\RecordSchema; +use yii\redis\RecordSchema; class Order extends ActiveRecord { diff --git a/tests/unit/data/ar/redis/OrderItem.php b/tests/unit/data/ar/redis/OrderItem.php index f0719b9..b77c216 100644 --- a/tests/unit/data/ar/redis/OrderItem.php +++ b/tests/unit/data/ar/redis/OrderItem.php @@ -2,7 +2,7 @@ namespace yiiunit\data\ar\redis; -use yii\db\redis\RecordSchema; +use yii\redis\RecordSchema; class OrderItem extends ActiveRecord { diff --git a/tests/unit/framework/db/redis/ActiveRecordTest.php b/tests/unit/framework/db/redis/ActiveRecordTest.php deleted file mode 100644 index e9a66e6..0000000 --- a/tests/unit/framework/db/redis/ActiveRecordTest.php +++ /dev/null @@ -1,422 +0,0 @@ -getConnection(); - - $customer = new Customer(); - $customer->setAttributes(array('email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1), false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(array('email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1), false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(array('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(array('name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1), false); - $item->save(false); - $item = new Item(); - $item->setAttributes(array('name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1), false); - $item->save(false); - $item = new Item(); - $item->setAttributes(array('name' => 'Ice Age', 'category_id' => 2), false); - $item->save(false); - $item = new Item(); - $item->setAttributes(array('name' => 'Toy Story', 'category_id' => 2), false); - $item->save(false); - $item = new Item(); - $item->setAttributes(array('name' => 'Cars', 'category_id' => 2), false); - $item->save(false); - - $order = new Order(); - $order->setAttributes(array('customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0), false); - $order->save(false); - $order = new Order(); - $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0), false); - $order->save(false); - $order = new Order(); - $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0), false); - $order->save(false); - - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0), false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0), false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0), false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0), false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0), false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(array('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); - - // find by column values - $customer = Customer::find(array('id' => 2)); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $customer = Customer::find(array('id' => 5)); - $this->assertNull($customer); - - // find by attributes -/* $customer = Customer::find()->where(array('name' => 'user2'))->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(2, $customer->id);*/ - - // find custom column -/* $customer = Customer::find()->select(array('*', '(status*2) AS status2')) - ->where(array('name' => 'user3'))->one(); - $this->assertEquals(3, $customer->id); - $this->assertEquals(4, $customer->status2);*/ - - // find count, sum, average, min, max, scalar - $this->assertEquals(3, Customer::find()->count()); -/* $this->assertEquals(2, Customer::find()->where('id=1 OR id=2')->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')); - $this->assertEquals(3, Customer::find()->select('COUNT(*)')->scalar());*/ - - // scope -// $this->assertEquals(2, Customer::find()->active()->count()); - - // asArray - $customer = Customer::find()->primaryKeys(array(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); - } - -// public function testFindLazy() -// { -// /** @var $customer Customer */ -// $customer = Customer::find(2); -// $orders = $customer->orders; -// $this->assertEquals(2, count($orders)); -// -// $orders = $customer->getOrders()->primaryKeys(array(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(array(), $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 testFindLazyViaTable() - { - /** @var $order Order * / - $order = Order::find(1); - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->books)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - - $order = Order::find(2); - $this->assertEquals(2, $order->id); - $this->assertEquals(0, count($order->books)); - } - - public function testFindEagerViaTable() - { - $orders = Order::find()->with('books')->all(); - $this->assertEquals(3, count($orders)); - - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->books)); - $this->assertEquals(1, $order->books[0]->id); - $this->assertEquals(2, $order->books[1]->id); - - $order = $orders[1]; - $this->assertEquals(2, $order->id); - $this->assertEquals(0, count($order->books)); - - $order = $orders[2]; - $this->assertEquals(3, $order->id); - $this->assertEquals(1, count($order->books)); - $this->assertEquals(2, $order->books[0]->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 table -// $order = Order::find(2); -// $this->assertEquals(0, count($order->books)); -// $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); -// $this->assertNull($orderItem); -// $item = Item::find(1); -// $order->link('books', $item, array('quantity' => 10, 'subtotal' => 100)); -// $this->assertEquals(1, count($order->books)); -// $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); -// $this->assertTrue($orderItem instanceof OrderItem); -// $this->assertEquals(10, $orderItem->quantity); -// $this->assertEquals(100, $orderItem->subtotal); -// -// // via model -// $order = Order::find(1); -// $this->assertEquals(2, count($order->items)); -// $this->assertEquals(2, count($order->orderItems)); -// $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); -// $this->assertNull($orderItem); -// $item = Item::find(3); -// $order->link('items', $item, array('quantity' => 10, 'subtotal' => 100)); -// $this->assertEquals(3, count($order->items)); -// $this->assertEquals(3, count($order->orderItems)); -// $orderItem = OrderItem::find(array('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)); -// -// // via table -// $order = Order::find(1); -// $this->assertEquals(2, count($order->books)); -// $order->unlink('books', $order->books[1], true); -// $this->assertEquals(1, count($order->books)); -// $this->assertEquals(1, 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); - - // updateCounters - $pk = array('order_id' => 2, 'item_id' => 4); - $orderItem = OrderItem::find($pk); - $this->assertEquals(1, $orderItem->quantity); - $ret = $orderItem->updateCounters(array('quantity' => -1)); - $this->assertTrue($ret); - $this->assertEquals(0, $orderItem->quantity); - $orderItem = OrderItem::find($pk); - $this->assertEquals(0, $orderItem->quantity); - - // updateAll - $customer = Customer::find(3); - $this->assertEquals('user3', $customer->name); - $ret = Customer::updateAll(array( - 'name' => 'temp', - ), array('id' => 3)); - $this->assertEquals(1, $ret); - $customer = Customer::find(3); - $this->assertEquals('temp', $customer->name); - - // updateCounters - $pk = array('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 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/db/redis/RedisConnectionTest.php b/tests/unit/framework/db/redis/RedisConnectionTest.php deleted file mode 100644 index 85c69ac..0000000 --- a/tests/unit/framework/db/redis/RedisConnectionTest.php +++ /dev/null @@ -1,66 +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/db/redis/RedisTestCase.php b/tests/unit/framework/db/redis/RedisTestCase.php deleted file mode 100644 index 309ecc5..0000000 --- a/tests/unit/framework/db/redis/RedisTestCase.php +++ /dev/null @@ -1,57 +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 \yii\db\redis\Connection; - $db->dsn = $params['dsn']; - $db->password = $params['password']; - if ($reset) { - $db->open(); - $db->flushall(); -/* $lines = explode(';', file_get_contents($params['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - }*/ - } - return $db; - } -} \ No newline at end of file diff --git a/tests/unit/framework/redis/ActiveRecordTest.php b/tests/unit/framework/redis/ActiveRecordTest.php new file mode 100644 index 0000000..2587878 --- /dev/null +++ b/tests/unit/framework/redis/ActiveRecordTest.php @@ -0,0 +1,422 @@ +getConnection(); + + $customer = new Customer(); + $customer->setAttributes(array('email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1), false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(array('email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1), false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(array('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(array('name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Ice Age', 'category_id' => 2), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Toy Story', 'category_id' => 2), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Cars', 'category_id' => 2), false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(array('customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0), false); + $order->save(false); + $order = new Order(); + $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0), false); + $order->save(false); + $order = new Order(); + $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0), false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('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); + + // find by column values + $customer = Customer::find(array('id' => 2)); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer = Customer::find(array('id' => 5)); + $this->assertNull($customer); + + // find by attributes +/* $customer = Customer::find()->where(array('name' => 'user2'))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(2, $customer->id);*/ + + // find custom column +/* $customer = Customer::find()->select(array('*', '(status*2) AS status2')) + ->where(array('name' => 'user3'))->one(); + $this->assertEquals(3, $customer->id); + $this->assertEquals(4, $customer->status2);*/ + + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); +/* $this->assertEquals(2, Customer::find()->where('id=1 OR id=2')->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')); + $this->assertEquals(3, Customer::find()->select('COUNT(*)')->scalar());*/ + + // scope +// $this->assertEquals(2, Customer::find()->active()->count()); + + // asArray + $customer = Customer::find()->primaryKeys(array(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); + } + +// public function testFindLazy() +// { +// /** @var $customer Customer */ +// $customer = Customer::find(2); +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// +// $orders = $customer->getOrders()->primaryKeys(array(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(array(), $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 testFindLazyViaTable() + { + /** @var $order Order * / + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(2); + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + } + + public function testFindEagerViaTable() + { + $orders = Order::find()->with('books')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->books[0]->id); + $this->assertEquals(2, $order->books[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertEquals(1, count($order->books)); + $this->assertEquals(2, $order->books[0]->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 table +// $order = Order::find(2); +// $this->assertEquals(0, count($order->books)); +// $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); +// $this->assertNull($orderItem); +// $item = Item::find(1); +// $order->link('books', $item, array('quantity' => 10, 'subtotal' => 100)); +// $this->assertEquals(1, count($order->books)); +// $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); +// $this->assertTrue($orderItem instanceof OrderItem); +// $this->assertEquals(10, $orderItem->quantity); +// $this->assertEquals(100, $orderItem->subtotal); +// +// // via model +// $order = Order::find(1); +// $this->assertEquals(2, count($order->items)); +// $this->assertEquals(2, count($order->orderItems)); +// $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); +// $this->assertNull($orderItem); +// $item = Item::find(3); +// $order->link('items', $item, array('quantity' => 10, 'subtotal' => 100)); +// $this->assertEquals(3, count($order->items)); +// $this->assertEquals(3, count($order->orderItems)); +// $orderItem = OrderItem::find(array('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)); +// +// // via table +// $order = Order::find(1); +// $this->assertEquals(2, count($order->books)); +// $order->unlink('books', $order->books[1], true); +// $this->assertEquals(1, count($order->books)); +// $this->assertEquals(1, 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); + + // updateCounters + $pk = array('order_id' => 2, 'item_id' => 4); + $orderItem = OrderItem::find($pk); + $this->assertEquals(1, $orderItem->quantity); + $ret = $orderItem->updateCounters(array('quantity' => -1)); + $this->assertTrue($ret); + $this->assertEquals(0, $orderItem->quantity); + $orderItem = OrderItem::find($pk); + $this->assertEquals(0, $orderItem->quantity); + + // updateAll + $customer = Customer::find(3); + $this->assertEquals('user3', $customer->name); + $ret = Customer::updateAll(array( + 'name' => 'temp', + ), array('id' => 3)); + $this->assertEquals(1, $ret); + $customer = Customer::find(3); + $this->assertEquals('temp', $customer->name); + + // updateCounters + $pk = array('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 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 new file mode 100644 index 0000000..a218899 --- /dev/null +++ b/tests/unit/framework/redis/RedisConnectionTest.php @@ -0,0 +1,66 @@ +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 new file mode 100644 index 0000000..f8e23b2 --- /dev/null +++ b/tests/unit/framework/redis/RedisTestCase.php @@ -0,0 +1,51 @@ +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