From 2f37d09367ef84f7b28ebbc6dcc8ce9e3d1b07d5 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 31 Dec 2011 21:08:37 -0500 Subject: [PATCH] ... --- framework/base/Dictionary.php | 2 +- framework/base/Object.php | 23 - framework/base/Vector.php | 2 +- framework/db/ar/ActiveQueryBuilder.php | 21 + framework/db/ar/ActiveRecord.php | 1888 +++++++++----------------- framework/db/dao/TableSchema.php | 16 + framework/util/Text.php | 20 +- tests/unit/data/mysql.sql | 250 ++-- tests/unit/framework/base/DictionaryTest.php | 4 +- tests/unit/framework/base/ObjectTest.php | 1 - tests/unit/framework/base/VectorTest.php | 4 +- tests/unit/framework/db/dao/CommandTest.php | 100 +- 12 files changed, 817 insertions(+), 1514 deletions(-) create mode 100644 framework/db/ar/ActiveQueryBuilder.php diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index cd5a5fb..d655f77 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -179,7 +179,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` * @throws Exception if data is neither an array nor an iterator. */ - public function fromArray($data) + public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_d !== array()) { diff --git a/framework/base/Object.php b/framework/base/Object.php index dd52410..af49553 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -342,27 +342,4 @@ class Object return $object; } - - /** - * Configures the object properties with the specified array. - * @param array $array name-value pairs to be used to initialize the properties of this object. - * @return Object the object itself - */ - public function fromArray($array) - { - foreach ($array as $name => $value) { - $this->$name = $value; - } - return $this; - } - - /** - * Returns the object in terms of an array. - * The default implementation will return the result of PHP function `get_object_vars()`. - * @return array the array representation of this object. - */ - public function toArray() - { - return get_object_vars($this); - } } diff --git a/framework/base/Vector.php b/framework/base/Vector.php index 7541c63..7c6dadd 100644 --- a/framework/base/Vector.php +++ b/framework/base/Vector.php @@ -240,7 +240,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` * @throws Exception if data is neither an array nor an object implementing `Traversable`. */ - public function fromArray($data) + public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_c > 0) { diff --git a/framework/db/ar/ActiveQueryBuilder.php b/framework/db/ar/ActiveQueryBuilder.php new file mode 100644 index 0000000..7c18b51 --- /dev/null +++ b/framework/db/ar/ActiveQueryBuilder.php @@ -0,0 +1,21 @@ + + * @since 2.0 + */ +class ActiveQueryBuilder extends \yii\base\Object +{ + +} \ No newline at end of file diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 90e4dd8..331dd09 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -4,52 +4,20 @@ * * @author Qiang Xue * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright © 2008-2012 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db\ar; -/* -1. query -$finder = Post::model()->find(array( - 'select' => 'id, title', - 'where' => 'id=123', - 'with' => array( - 'creator' => array( - - ), - ), -)); -$post = $finder->first(); -$posts = $finder->all(); -foreach($finder as $post) { - -} -foreach($finder->next(5) as $posts) { - -} - - -2. insert -$post = Post::create(); -$post->attributes = $_POST['Post']; -$post->save(); - -3. update - -4. delete - - -*/ +use yii\db\Exception; +use yii\db\dao\Connection; +use yii\db\dao\TableSchema; +use yii\db\dao\Query; /** * ActiveRecord is the base class for classes representing relational data. * - * It implements the active record design pattern, a popular Object-Relational Mapping (ORM) technique. - * Please check {@link http://www.yiiframework.com/doc/guide/database.ar the Guide} for more details - * about this class. - * * @author Qiang Xue * @since 2.0 * @@ -57,277 +25,88 @@ $post->save(); */ abstract class ActiveRecord extends \yii\base\Model { - const BELONGS_TO = 'CBelongsToRelation'; - const HAS_ONE = 'CHasOneRelation'; - const HAS_MANY = 'CHasManyRelation'; - const MANY_MANY = 'CManyManyRelation'; - - /** - * @var CDbConnection the default database connection for all active record classes. - * By default, this is the 'db' application component. - * @see getDbConnection - */ - public static $db; - - private static $_models = array(); // class name => model - - private $_md; // meta data - private $_new = false; // whether this instance is new or not - private $_attributes = array(); // attribute name => attribute value - private $_related = array(); // attribute name => related objects - private $_c; // query criteria (used by finder only) - private $_pk; // old primary key value - private $_alias = 't'; // the table alias being used for query - - /** - * Constructor. - * @param string $scenario scenario name. See {@link CModel::scenario} for more details about this parameter. + * @var */ - public function __construct($scenario = 'insert') - { - if ($scenario === null) // internally used by populateRecord() and model() - return; + private static $_md; - $this->setScenario($scenario); - $this->setIsNewRecord(true); - $this->_attributes = $this->getMetaData()->attributeDefaults; - - $this->init(); - - $this->attachBehaviors($this->behaviors()); - $this->afterConstruct(); - } - - /** - * Sets the parameters about query caching. - * This is a shortcut method to {@link CDbConnection::cache()}. - * It changes the query caching parameter of the {@link dbConnection} instance. - * @param integer $duration the number of seconds that query results may remain valid in cache. - * If this is 0, the caching will be disabled. - * @param CCacheDependency $dependency the dependency that will be used when saving the query results into cache. - * @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1, - * meaning that the next SQL query will be cached. - * @return ActiveRecord the active record instance itself. - */ - public function cache($duration, $dependency = null, $queryCount = 1) - { - $this->getDbConnection()->cache($duration, $dependency, $queryCount); - return $this; - } + private $_new = false; // whether this instance is new or not + private $_attributes = array(); // attribute name => attribute value + private $_oldAttributes; + private $_related = array(); // attribute name => related objects + private $_pk; // old primary key value /** - * PHP sleep magic method. - * This method ensures that the model meta data reference is set to null. - * @return array + * Returns the metadata for this AR class. + * @param boolean $refresh whether to rebuild the metadata. + * @return ActiveMetaData the meta for this AR class. */ - public function __sleep() + public static function getMetaData($refresh = false) { - $this->_md = null; - return array_keys((array)$this); + $class = get_called_class(); + if (!$refresh && isset(self::$_md[$class])) { + return self::$_md[$class]; + } else { + return self::$_md[$class] = new ActiveMetaData('\\' . $class); + } } - /** - * PHP getter magic method. - * This method is overridden so that AR attributes can be accessed like properties. - * @param string $name property name - * @return mixed property value - * @see getAttribute - */ - public function __get($name) + public static function find($query = null) { - if (isset($this->_attributes[$name])) - return $this->_attributes[$name]; - elseif (isset($this->getMetaData()->columns[$name])) - return null; - elseif (isset($this->_related[$name])) - return $this->_related[$name]; - elseif (isset($this->getMetaData()->relations[$name])) - return $this->getRelated($name); - else - return parent::__get($name); + $finder = static::createFinder(); + if ($query instanceof Query) { + $finder->query = $query; + } elseif ($query !== null) { + // todo: findByPk + } + return $finder; } - /** - * PHP setter magic method. - * This method is overridden so that AR attributes can be accessed like properties. - * @param string $name property name - * @param mixed $value property value - */ - public function __set($name, $value) + public static function findBySql($sql, $params = array()) { - if ($this->setAttribute($name, $value) === false) - { - if (isset($this->getMetaData()->relations[$name])) - $this->_related[$name] = $value; - else - parent::__set($name, $value); + $finder = static::createFinder(); + if (!is_array($params)) { + $params = func_get_args(); + array_shift($params); } + $finder->setSql($sql); + return $finder->params($params); } - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named attribute is null or not. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) + public static function exists($condition, $params) { - if (isset($this->_attributes[$name])) - return true; - elseif (isset($this->getMetaData()->columns[$name])) - return false; - elseif (isset($this->_related[$name])) - return true; - elseif (isset($this->getMetaData()->relations[$name])) - return $this->getRelated($name) !== null; - else - return parent::__isset($name); - } - /** - * Sets a component property to be null. - * This method overrides the parent implementation by clearing - * the specified attribute value. - * @param string $name the property name or the event name - */ - public function __unset($name) - { - if (isset($this->getMetaData()->columns[$name])) - unset($this->_attributes[$name]); - elseif (isset($this->getMetaData()->relations[$name])) - unset($this->_related[$name]); - else - parent::__unset($name); } - /** - * Calls the named method which is not a class method. - * Do not call this method. This is a PHP magic method that we override - * to implement the named scope feature. - * @param string $name the method name - * @param array $parameters method parameters - * @return mixed the method return value - */ - public function __call($name, $parameters) + public static function updateAll() { - if (isset($this->getMetaData()->relations[$name])) - { - if (empty($parameters)) - return $this->getRelated($name, false); - else - return $this->getRelated($name, false, $parameters[0]); - } - - $scopes = $this->scopes(); - if (isset($scopes[$name])) - { - $this->getDbCriteria()->mergeWith($scopes[$name]); - return $this; - } - return parent::__call($name, $parameters); } - /** - * Returns the related record(s). - * This method will return the related record(s) of the current record. - * If the relation is HAS_ONE or BELONGS_TO, it will return a single object - * or null if the object does not exist. - * If the relation is HAS_MANY or MANY_MANY, it will return an array of objects - * or an empty array. - * @param string $name the relation name (see {@link relations}) - * @param boolean $refresh whether to reload the related objects from database. Defaults to false. - * @param array $params additional parameters that customize the query conditions as specified in the relation declaration. - * This parameter has been available since version 1.0.5. - * @return mixed the related object(s). - * @throws CDbException if the relation is not specified in {@link relations}. - */ - public function getRelated($name, $refresh = false, $params = array()) + public static function updateCounters() { - if (!$refresh && $params === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))) - return $this->_related[$name]; - - $md = $this->getMetaData(); - if (!isset($md->relations[$name])) - throw new CDbException(Yii::t('yii', '{class} does not have relation "{name}".', - array('{class}' => get_class($this), '{name}' => $name))); - - Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'system.db.ar.ActiveRecord'); - $relation = $md->relations[$name]; - if ($this->getIsNewRecord() && !$refresh && ($relation instanceof CHasOneRelation || $relation instanceof CHasManyRelation)) - return $relation instanceof CHasOneRelation ? null : array(); - - if ($params !== array()) // dynamic query - { - $exists = isset($this->_related[$name]) || array_key_exists($name, $this->_related); - if ($exists) - $save = $this->_related[$name]; - $r = array($name => $params); - } else - $r = $name; - unset($this->_related[$name]); - - $finder = new CActiveFinder($this, $r); - $finder->lazyFind($this); - - if (!isset($this->_related[$name])) - { - if ($relation instanceof CHasManyRelation) - $this->_related[$name] = array(); - elseif ($relation instanceof CStatRelation) - $this->_related[$name] = $relation->defaultValue; - else - $this->_related[$name] = null; - } - if ($params !== array()) - { - $results = $this->_related[$name]; - if ($exists) - $this->_related[$name] = $save; - else - unset($this->_related[$name]); - return $results; - } else - return $this->_related[$name]; } - /** - * Returns a value indicating whether the named related object(s) has been loaded. - * @param string $name the relation name - * @return boolean a value indicating whether the named related object(s) has been loaded. - */ - public function hasRelated($name) + public static function deleteAll() { - return isset($this->_related[$name]) || array_key_exists($name, $this->_related); + } - /** - * Returns the query criteria associated with this model. - * @param boolean $createIfNull whether to create a criteria instance if it does not exist. Defaults to true. - * @return CDbCriteria the query criteria that is associated with this model. - * This criteria is mainly used by {@link scopes named scope} feature to accumulate - * different criteria specifications. - */ - public function getDbCriteria($createIfNull = true) + public static function createFinder() { - if ($this->_c === null) - { - if (($c = $this->defaultScope()) !== array() || $createIfNull) - $this->_c = new CDbCriteria($c); - } - return $this->_c; + return new ActiveFinder('\\' . get_called_class()); } /** - * Sets the query criteria for the current model. - * @param CDbCriteria $criteria the query criteria + * Returns the database connection used by active record. + * By default, the "db" 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 active record. */ - public function setDbCriteria($criteria) + public static function getDbConnection() { - $this->_c = $criteria; + return \Yii::$application->getDb(); } /** @@ -338,93 +117,26 @@ abstract class ActiveRecord extends \yii\base\Model * @return array the query criteria. This will be used as the parameter to the constructor * of {@link CDbCriteria}. */ - public function defaultScope() + public static function defaultScope() { return array(); } /** - * Resets all scopes and criterias applied including default scope. - * - * @return ActiveRecord - */ - public function resetScope() - { - $this->_c = new CDbCriteria(); - return $this; - } - - /** - * Returns the static model of the specified AR class. - * The model returned is a static instance of the AR class. - * It is provided for invoking class-level methods (something similar to static class methods.) - * - * EVERY derived AR class must override this method as follows, - *
-	 * public static function model($className=__CLASS__)
-	 * {
-	 *     return parent::model($className);
-	 * }
-	 * 
- * - * @param string $className active record class name. - * @return ActiveRecord active record model instance. - */ - public static function model($className = __CLASS__) - { - if (isset(self::$_models[$className])) - return self::$_models[$className]; - else - { - $model = self::$_models[$className] = new $className(null); - $model->_md = new ActiveRecordMetaData($model); - $model->attachBehaviors($model->behaviors()); - return $model; - } - } - - /** - * Returns the meta-data for this AR - * @return ActiveRecordMetaData the meta for this AR class. - */ - public function getMetaData() - { - if ($this->_md !== null) - return $this->_md; - else - return $this->_md = self::model(get_class($this))->_md; - } - - /** - * Refreshes the meta data for this AR class. - * By calling this method, this AR class will regenerate the meta data needed. - * This is useful if the table schema has been changed and you want to use the latest - * available table schema. Make sure you have called {@link CDbSchema::refresh} - * before you call this method. Otherwise, old table schema data will still be used. - */ - public function refreshMetaData() - { - $finder = self::model(get_class($this)); - $finder->_md = new ActiveRecordMetaData($finder); - if ($this !== $finder) - $this->_md = $finder->_md; - } - - /** * Returns the name of the associated database table. * By default this method returns the class name as the table name. * You may override this method if the table is not named after this convention. * @return string the table name */ - public function tableName() + public static function tableName() { - return get_class($this); + return basename(get_called_class()); } /** * Returns the primary key of the associated database table. * This method is meant to be overridden in case when the table is not defined with a primary key - * (for some legency database). If the table is already defined with a primary key, + * (for some legacy database). If the table is already defined with a primary key, * you do not need to override this method. The default implementation simply returns null, * meaning using the primary key defined in the database. * @return mixed the primary key of the associated database table. @@ -432,7 +144,7 @@ abstract class ActiveRecord extends \yii\base\Model * If the key is a composite one consisting of several columns, it should * return the array of the key column names. */ - public function primaryKey() + public static function primaryKey() { } @@ -479,7 +191,7 @@ abstract class ActiveRecord extends \yii\base\Model *
  • 'alias': the alias for the table associated with this relationship. * This option has been available since version 1.0.1. It defaults to null, * meaning the table alias is the same as the relation name.
  • - *
  • 'params': the parameters to be bound to the generated SQL statement. + *
  • 'params': the parameters to be bound to the generated SQL statement. * This should be given as an array of name-value pairs. This option has been * available since version 1.0.3.
  • *
  • 'on': the ON clause. The condition specified here will be appended @@ -507,15 +219,15 @@ abstract class ActiveRecord extends \yii\base\Model * Below is an example declaring related objects for 'Post' active record class: *
     	 * return array(
    -	 *     'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
    -	 *     'comments'=>array(self::HAS_MANY, 'Comment', 'post_id', 'with'=>'author', 'order'=>'create_time DESC'),
    -	 *     'tags'=>array(self::MANY_MANY, 'Tag', 'post_tag(post_id, tag_id)', 'order'=>'name'),
    +	 *	 'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
    +	 *	 'comments'=>array(self::HAS_MANY, 'Comment', 'post_id', 'with'=>'author', 'order'=>'create_time DESC'),
    +	 *	 'tags'=>array(self::MANY_MANY, 'Tag', 'post_tag(post_id, tag_id)', 'order'=>'name'),
     	 * );
     	 * 
    * * @return array list of related object declarations. Defaults to empty array. */ - public function relations() + public static function relations() { return array(); } @@ -529,13 +241,13 @@ abstract class ActiveRecord extends \yii\base\Model * 'published'. *
     	 * return array(
    -	 *     'published'=>array(
    -	 *           'condition'=>'status=1',
    -	 *     ),
    -	 *     'recently'=>array(
    -	 *           'order'=>'create_time DESC',
    -	 *           'limit'=>5,
    -	 *     ),
    +	 *	 'published'=>array(
    +	 *		   'condition'=>'status=1',
    +	 *	 ),
    +	 *	 'recently'=>array(
    +	 *		   'order'=>'create_time DESC',
    +	 *		   'limit'=>5,
    +	 *	 ),
     	 * );
     	 * 
    * If the above scopes are declared in a 'Post' model, we can perform the following @@ -551,71 +263,283 @@ abstract class ActiveRecord extends \yii\base\Model * values are the corresponding scope definitions. Each scope definition is represented * as an array whose keys must be properties of {@link CDbCriteria}. */ - public function scopes() + public static function scopes() { return array(); } /** - * Returns the list of all attribute names of the model. - * This would return all column names of the table associated with this AR class. - * @return array list of attribute names. + * Constructor. + * @param string $scenario scenario name. See {@link CModel::scenario} for more details about this parameter. */ - public function attributeNames() + public function __construct($scenario = 'insert') { - return array_keys($this->getMetaData()->columns); + if ($scenario === null) // internally used by populateRecord() and model() + { + return; + } + + $this->setScenario($scenario); + $this->setIsNewRecord(true); } /** - * Returns the text label for the specified attribute. - * This method overrides the parent implementation by supporting - * returning the label defined in relational object. - * In particular, if the attribute name is in the form of "post.author.name", - * then this method will derive the label from the "author" relation's "name" attribute. - * @param string $attribute the attribute name - * @return string the attribute label - * @see generateAttributeLabel + * PHP sleep magic method. + * This method ensures that the model meta data reference is set to null. + * @return array */ - public function getAttributeLabel($attribute) + public function __sleep() { - $labels = $this->attributeLabels(); - if (isset($labels[$attribute])) - return $labels[$attribute]; - elseif (strpos($attribute, '.') !== false) - { - $segs = explode('.', $attribute); - $name = array_pop($segs); - $model = $this; - foreach ($segs as $seg) - { - $relations = $model->getMetaData()->relations; - if (isset($relations[$seg])) - $model = ActiveRecord::model($relations[$seg]->className); - else - break; - } - return $model->getAttributeLabel($name); - } else - return $this->generateAttributeLabel($attribute); + return array_keys((array)$this); } /** - * Returns the database connection used by active record. - * By default, the "db" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return CDbConnection the database connection used by active record. - */ - public function getDbConnection() + * PHP getter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @return mixed property value + * @see getAttribute + */ + public function __get($name) + { + if (isset($this->_attributes[$name])) { + return $this->_attributes[$name]; + } elseif (isset($this->getMetaData()->table->columns[$name])) { + return null; + } elseif (isset($this->_related[$name])) { + return $this->_related[$name]; + } elseif (isset($this->getMetaData()->relations[$name])) { + return $this->getRelatedRecord($name); + } else { + return parent::__get($name); + } + } + + /** + * PHP setter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @param mixed $value property value + */ + public function __set($name, $value) + { + if (isset($this->getMetaData()->table->columns[$name])) { + $this->_attributes[$name] = $value; + } elseif (isset($this->getMetaData()->relations[$name])) { + $this->_related[$name] = $value; + } else { + parent::__set($name, $value); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named attribute is null or not. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) { - if (self::$db !== null) - return self::$db; + if (isset($this->_attributes[$name])) { + return true; + } + elseif (isset($this->getMetaData()->columns[$name])) + { + return false; + } + elseif (isset($this->_related[$name])) + { + return true; + } + elseif (isset($this->getMetaData()->relations[$name])) + { + return $this->getRelatedRecord($name) !== null; + } else { - self::$db = \Yii::$application->getDb(); - if (self::$db instanceof CDbConnection) - return self::$db; + return parent::__isset($name); + } + } + + /** + * Sets a component property to be null. + * This method overrides the parent implementation by clearing + * the specified attribute value. + * @param string $name the property name or the event name + */ + public function __unset($name) + { + if (isset($this->getMetaData()->columns[$name])) { + unset($this->_attributes[$name]); + } + elseif (isset($this->getMetaData()->relations[$name])) + { + unset($this->_related[$name]); + } + else + { + parent::__unset($name); + } + } + + /** + * Calls the named method which is not a class method. + * Do not call this method. This is a PHP magic method that we override + * to implement the named scope feature. + * @param string $name the method name + * @param array $parameters method parameters + * @return mixed the method return value + */ + public function __call($name, $parameters) + { + if (isset($this->getMetaData()->relations[$name])) { + if (empty($parameters)) { + return $this->getRelatedRecord($name, false); + } + else + { + return $this->getRelatedRecord($name, false, $parameters[0]); + } + } + + $scopes = $this->scopes(); + if (isset($scopes[$name])) { + $this->getDbCriteria()->mergeWith($scopes[$name]); + return $this; + } + + return parent::__call($name, $parameters); + } + + /** + * Returns the related record(s). + * This method will return the related record(s) of the current record. + * If the relation is HAS_ONE or BELONGS_TO, it will return a single object + * or null if the object does not exist. + * If the relation is HAS_MANY or MANY_MANY, it will return an array of objects + * or an empty array. + * @param string $name the relation name (see {@link relations}) + * @param boolean $refresh whether to reload the related objects from database. Defaults to false. + * @param array $params additional parameters that customize the query conditions as specified in the relation declaration. + * This parameter has been available since version 1.0.5. + * @return mixed the related object(s). + * @throws Exception if the relation is not specified in {@link relations}. + */ + public function getRelatedRecord($name, $refresh = false, $params = array()) + { + if (!$refresh && $params === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))) { + return $this->_related[$name]; + } + + $md = $this->getMetaData(); + if (!isset($md->relations[$name])) { + throw new Exception(Yii::t('yii', '{class} does not have relation "{name}".', + array('{class}' => get_class($this), '{name}' => $name))); + } + + Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'system.db.ar.ActiveRecord'); + $relation = $md->relations[$name]; + if ($this->getIsNewRecord() && !$refresh && ($relation instanceof CHasOneRelation || $relation instanceof CHasManyRelation)) { + return $relation instanceof CHasOneRelation ? null : array(); + } + + if ($params !== array()) // dynamic query + { + $exists = isset($this->_related[$name]) || array_key_exists($name, $this->_related); + if ($exists) { + $save = $this->_related[$name]; + } + $r = array($name => $params); + } else + { + $r = $name; + } + unset($this->_related[$name]); + + $finder = new CActiveFinder($this, $r); + $finder->lazyFind($this); + + if (!isset($this->_related[$name])) { + if ($relation instanceof CHasManyRelation) { + $this->_related[$name] = array(); + } + elseif ($relation instanceof CStatRelation) + { + $this->_related[$name] = $relation->defaultValue; + } else - throw new CDbException(Yii::t('yii', 'Active Record requires a "db" CDbConnection application component.')); + { + $this->_related[$name] = null; + } + } + + if ($params !== array()) { + $results = $this->_related[$name]; + if ($exists) { + $this->_related[$name] = $save; + } + else + { + unset($this->_related[$name]); + } + return $results; + } else + { + return $this->_related[$name]; + } + } + + /** + * Returns a value indicating whether the named related object(s) has been loaded. + * @param string $name the relation name + * @return boolean a value indicating whether the named related object(s) has been loaded. + */ + public function hasRelated($name) + { + return isset($this->_related[$name]) || array_key_exists($name, $this->_related); + } + + /** + * Returns the list of all attribute names of the model. + * This would return all column names of the table associated with this AR class. + * @return array list of attribute names. + */ + public function attributeNames() + { + return array_keys($this->getMetaData()->columns); + } + + /** + * Returns the text label for the specified attribute. + * This method overrides the parent implementation by supporting + * returning the label defined in relational object. + * In particular, if the attribute name is in the form of "post.author.name", + * then this method will derive the label from the "author" relation's "name" attribute. + * @param string $attribute the attribute name + * @return string the attribute label + * @see generateAttributeLabel + */ + public function getAttributeLabel($attribute) + { + $labels = $this->attributeLabels(); + if (isset($labels[$attribute])) { + return $labels[$attribute]; + } elseif (strpos($attribute, '.') !== false) { + $segs = explode('.', $attribute); + $name = array_pop($segs); + $model = $this; + foreach ($segs as $seg) { + $relations = $model->getMetaData()->relations; + if (isset($relations[$seg])) { + $model = ActiveRecord::model($relations[$seg]->className); + } else { + break; + } + } + return $model->getAttributeLabel($name); + } else { + return $this->generateAttributeLabel($attribute); } } @@ -639,15 +563,6 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * Returns the command builder used by this AR. - * @return CDbCommandBuilder the command builder used by this AR - */ - public function getCommandBuilder() - { - return $this->getDbConnection()->getSchema()->getCommandBuilder(); - } - - /** * Checks whether this AR has the named attribute * @param string $name attribute name * @return boolean whether this AR has the named attribute (table column). @@ -670,10 +585,13 @@ abstract class ActiveRecord extends \yii\base\Model */ public function getAttribute($name) { - if (property_exists($this, $name)) + if (property_exists($this, $name)) { return $this->$name; + } elseif (isset($this->_attributes[$name])) + { return $this->_attributes[$name]; + } } /** @@ -686,42 +604,21 @@ abstract class ActiveRecord extends \yii\base\Model */ public function setAttribute($name, $value) { - if (property_exists($this, $name)) + if (property_exists($this, $name)) { $this->$name = $value; - elseif (isset($this->getMetaData()->columns[$name])) + } + elseif (isset($this->getMetaData()->table->columns[$name])) + { $this->_attributes[$name] = $value; + } else + { return false; + } return true; } /** - * Do not call this method. This method is used internally by {@link CActiveFinder} to populate - * related objects. This method adds a related object to this record. - * @param string $name attribute name - * @param mixed $record the related record - * @param mixed $index the index value in the related object collection. - * If true, it means using zero-based integer index. - * If false, it means a HAS_ONE or BELONGS_TO object and no index is needed. - */ - public function addRelatedRecord($name, $record, $index) - { - if ($index !== false) - { - if (!isset($this->_related[$name])) - $this->_related[$name] = array(); - if ($record instanceof ActiveRecord) - { - if ($index === true) - $this->_related[$name][] = $record; - else - $this->_related[$name][$index] = $record; - } - } elseif (!isset($this->_related[$name])) - $this->_related[$name] = $record; - } - - /** * Returns all column attribute values. * Note, related objects are not returned. * @param mixed $names names of attributes whose value needs to be returned. @@ -735,24 +632,31 @@ abstract class ActiveRecord extends \yii\base\Model $attributes = $this->_attributes; foreach ($this->getMetaData()->columns as $name => $column) { - if (property_exists($this, $name)) + if (property_exists($this, $name)) { $attributes[$name] = $this->$name; + } elseif ($names === true && !isset($attributes[$name])) + { $attributes[$name] = null; + } } - if (is_array($names)) - { + if (is_array($names)) { $attrs = array(); foreach ($names as $name) { - if (property_exists($this, $name)) + if (property_exists($this, $name)) { $attrs[$name] = $this->$name; + } else + { $attrs[$name] = isset($attributes[$name]) ? $attributes[$name] : null; + } } return $attrs; } else + { return $attributes; + } } /** @@ -780,10 +684,13 @@ abstract class ActiveRecord extends \yii\base\Model */ public function save($runValidation = true, $attributes = null) { - if (!$runValidation || $this->validate($attributes)) + if (!$runValidation || $this->validate($attributes)) { return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes); + } else + { return false; + } } /** @@ -847,29 +754,6 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * This event is raised before an AR finder performs a find call. - * In this event, the {@link CModelEvent::criteria} property contains the query criteria - * passed as parameters to those find methods. If you want to access - * the query criteria specified in scopes, please use {@link getDbCriteria()}. - * You can modify either criteria to customize them based on needs. - * @param CModelEvent $event the event parameter - * @see beforeFind - */ - public function onBeforeFind($event) - { - $this->raiseEvent('onBeforeFind', $event); - } - - /** - * This event is raised after the record is instantiated by a find method. - * @param CEvent $event the event parameter - */ - public function onAfterFind($event) - { - $this->raiseEvent('onAfterFind', $event); - } - - /** * This method is invoked before saving a record (after validation, if any). * The default implementation raises the {@link onBeforeSave} event. * You may override this method to do any preparation work for record saving. @@ -880,13 +764,14 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function beforeSave() { - if ($this->hasEventHandler('onBeforeSave')) - { + if ($this->hasEventHandler('onBeforeSave')) { $event = new CModelEvent($this); $this->onBeforeSave($event); return $event->isValid; } else + { return true; + } } /** @@ -897,8 +782,9 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function afterSave() { - if ($this->hasEventHandler('onAfterSave')) + if ($this->hasEventHandler('onAfterSave')) { $this->onAfterSave(new CEvent($this)); + } } /** @@ -910,13 +796,14 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function beforeDelete() { - if ($this->hasEventHandler('onBeforeDelete')) - { + if ($this->hasEventHandler('onBeforeDelete')) { $event = new CModelEvent($this); $this->onBeforeDelete($event); return $event->isValid; } else + { return true; + } } /** @@ -927,8 +814,9 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function afterDelete() { - if ($this->hasEventHandler('onAfterDelete')) + if ($this->hasEventHandler('onAfterDelete')) { $this->onAfterDelete(new CEvent($this)); + } } /** @@ -944,8 +832,7 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function beforeFind() { - if ($this->hasEventHandler('onBeforeFind')) - { + if ($this->hasEventHandler('onBeforeFind')) { $event = new CModelEvent($this); // for backward compatibility $event->criteria = func_num_args() > 0 ? func_get_arg(0) : null; @@ -961,26 +848,9 @@ abstract class ActiveRecord extends \yii\base\Model */ protected function afterFind() { - if ($this->hasEventHandler('onAfterFind')) + if ($this->hasEventHandler('onAfterFind')) { $this->onAfterFind(new CEvent($this)); - } - - /** - * Calls {@link beforeFind}. - * This method is internally used. - */ - public function beforeFindInternal() - { - $this->beforeFind(); - } - - /** - * Calls {@link afterFind}. - * This method is internally used. - */ - public function afterFindInternal() - { - $this->afterFind(); + } } /** @@ -997,27 +867,25 @@ abstract class ActiveRecord extends \yii\base\Model */ public function insert($attributes = null) { - if (!$this->getIsNewRecord()) - throw new CDbException(Yii::t('yii', 'The active record cannot be inserted to database because it is not new.')); - if ($this->beforeSave()) - { + if (!$this->getIsNewRecord()) { + throw new Exception(Yii::t('yii', 'The active record cannot be inserted to database because it is not new.')); + } + if ($this->beforeSave()) { Yii::trace(get_class($this) . '.insert()', 'system.db.ar.ActiveRecord'); $builder = $this->getCommandBuilder(); $table = $this->getMetaData()->tableSchema; $command = $builder->createInsertCommand($table, $this->getAttributes($attributes)); - if ($command->execute()) - { + if ($command->execute()) { $primaryKey = $table->primaryKey; - if ($table->sequenceName !== null) - { - if (is_string($primaryKey) && $this->$primaryKey === null) + if ($table->sequenceName !== null) { + if (is_string($primaryKey) && $this->$primaryKey === null) { $this->$primaryKey = $builder->getLastInsertID($table); + } elseif (is_array($primaryKey)) { foreach ($primaryKey as $pk) { - if ($this->$pk === null) - { + if ($this->$pk === null) { $this->$pk = $builder->getLastInsertID($table); break; } @@ -1045,19 +913,22 @@ abstract class ActiveRecord extends \yii\base\Model */ public function update($attributes = null) { - if ($this->getIsNewRecord()) - throw new CDbException(Yii::t('yii', 'The active record cannot be updated because it is new.')); - if ($this->beforeSave()) - { + if ($this->getIsNewRecord()) { + throw new Exception(Yii::t('yii', 'The active record cannot be updated because it is new.')); + } + if ($this->beforeSave()) { Yii::trace(get_class($this) . '.update()', 'system.db.ar.ActiveRecord'); - if ($this->_pk === null) + if ($this->_pk === null) { $this->_pk = $this->getPrimaryKey(); + } $this->updateByPk($this->getOldPrimaryKey(), $this->getAttributes($attributes)); $this->_pk = $this->getPrimaryKey(); $this->afterSave(); return true; } else + { return false; + } } /** @@ -1080,670 +951,196 @@ abstract class ActiveRecord extends \yii\base\Model */ public function saveAttributes($attributes) { - if (!$this->getIsNewRecord()) - { + if (!$this->getIsNewRecord()) { Yii::trace(get_class($this) . '.saveAttributes()', 'system.db.ar.ActiveRecord'); $values = array(); foreach ($attributes as $name => $value) { - if (is_integer($name)) + if (is_integer($name)) { $values[$value] = $this->$value; + } else - $values[$name] = $this->$name = $value; - } - if ($this->_pk === null) - $this->_pk = $this->getPrimaryKey(); - if ($this->updateByPk($this->getOldPrimaryKey(), $values) > 0) - { - $this->_pk = $this->getPrimaryKey(); - return true; - } else - return false; - } else - throw new CDbException(Yii::t('yii', 'The active record cannot be updated because it is new.')); - } - - /** - * Saves one or several counter columns for the current AR object. - * Note that this method differs from {@link updateCounters} in that it only - * saves the current AR object. - * An example usage is as follows: - *
    -	 * $postRecord=Post::model()->findByPk($postID);
    -	 * $postRecord->saveCounters(array('view_count'=>1));
    -	 * 
    - * Use negative values if you want to decrease the counters. - * @param array $counters the counters to be updated (column name=>increment value) - * @return boolean whether the saving is successful - * @see updateCounters - */ - public function saveCounters($counters) - { - Yii::trace(get_class($this) . '.saveCounters()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $table = $this->getTableSchema(); - $criteria = $builder->createPkCriteria($table, $this->getOldPrimaryKey()); - $command = $builder->createUpdateCounterCommand($this->getTableSchema(), $counters, $criteria); - if ($command->execute()) - { - foreach ($counters as $name => $value) - $this->$name = $this->$name + $value; - return true; - } else - return false; - } - - /** - * Deletes the row corresponding to this active record. - * @return boolean whether the deletion is successful. - * @throws CException if the record is new - */ - public function delete() - { - if (!$this->getIsNewRecord()) - { - Yii::trace(get_class($this) . '.delete()', 'system.db.ar.ActiveRecord'); - if ($this->beforeDelete()) - { - $result = $this->deleteByPk($this->getPrimaryKey()) > 0; - $this->afterDelete(); - return $result; - } else - return false; - } else - throw new CDbException(Yii::t('yii', 'The active record cannot be deleted because it is new.')); - } - - /** - * Repopulates this active record with the latest data. - * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record. - */ - public function refresh() - { - Yii::trace(get_class($this) . '.refresh()', 'system.db.ar.ActiveRecord'); - if (!$this->getIsNewRecord() && ($record = $this->findByPk($this->getPrimaryKey())) !== null) - { - $this->_attributes = array(); - $this->_related = array(); - foreach ($this->getMetaData()->columns as $name => $column) - { - if (property_exists($this, $name)) - $this->$name = $record->$name; - else - $this->_attributes[$name] = $record->$name; - } - return true; - } else - return false; - } - - /** - * Compares current active record with another one. - * The comparison is made by comparing table name and the primary key values of the two active records. - * @param ActiveRecord $record record to compare to - * @return boolean whether the two active records refer to the same row in the database table. - */ - public function equals($record) - { - return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); - } - - /** - * Returns the primary key value. - * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite. - * If primary key is not defined, null will be returned. - */ - public function getPrimaryKey() - { - $table = $this->getMetaData()->tableSchema; - if (is_string($table->primaryKey)) - return $this-> {$table->primaryKey}; - elseif (is_array($table->primaryKey)) - { - $values = array(); - foreach ($table->primaryKey as $name) - $values[$name] = $this->$name; - return $values; - } else - return null; - } - - /** - * Sets the primary key value. - * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}. - * @param mixed $value the new primary key value. If the primary key is composite, the new value - * should be provided as an array (column name=>column value). - */ - public function setPrimaryKey($value) - { - $this->_pk = $this->getPrimaryKey(); - $table = $this->getMetaData()->tableSchema; - if (is_string($table->primaryKey)) - $this-> {$table->primaryKey} = $value; - elseif (is_array($table->primaryKey)) - { - foreach ($table->primaryKey as $name) - $this->$name = $value[$name]; - } - } - - /** - * Returns the old primary key value. - * This refers to the primary key value that is populated into the record - * after executing a find method (e.g. find(), findAll()). - * The value remains unchanged even if the primary key attribute is manually assigned with a different value. - * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. - * If primary key is not defined, null will be returned. - */ - public function getOldPrimaryKey() - { - return $this->_pk; - } - - /** - * Sets the old primary key value. - * @param mixed $value the old primary key value. - */ - public function setOldPrimaryKey($value) - { - $this->_pk = $value; - } - - /** - * Performs the actual DB query and populates the AR objects with the query result. - * This method is mainly internally used by other AR query methods. - * @param CDbCriteria $criteria the query criteria - * @param boolean $all whether to return all data - * @return mixed the AR objects populated with the query result - */ - protected function query($criteria, $all = false) - { - $this->beforeFind(); - $this->applyScopes($criteria); - if (empty($criteria->with)) - { - if (!$all) - $criteria->limit = 1; - $command = $this->getCommandBuilder()->createFindCommand($this->getTableSchema(), $criteria); - return $all ? $this->populateRecords($command->queryAll(), true, $criteria->index) : $this->populateRecord($command->queryRow()); - } else - { - $finder = new CActiveFinder($this, $criteria->with); - return $finder->query($criteria, $all); - } - } - - /** - * Applies the query scopes to the given criteria. - * This method merges {@link dbCriteria} with the given criteria parameter. - * It then resets {@link dbCriteria} to be null. - * @param CDbCriteria $criteria the query criteria. This parameter may be modified by merging {@link dbCriteria}. - */ - public function applyScopes(&$criteria) - { - if (!empty($criteria->scopes)) - { - $scs = $this->scopes(); - $c = $this->getDbCriteria(); - foreach ((array)$criteria->scopes as $k => $v) - { - if (is_integer($k)) - { - if (is_string($v)) - { - if (isset($scs[$v])) - { - $c->mergeWith($scs[$v], true); - continue; - } - $scope = $v; - $params = array(); - } elseif (is_array($v)) - { - $scope = key($v); - $params = current($v); - } - } elseif (is_string($k)) { - $scope = $k; - $params = $v; + $values[$name] = $this->$name = $value; } - - call_user_func_array(array($this, $scope), (array)$params); } - } - - if (isset($c) || ($c = $this->getDbCriteria(false)) !== null) - { - $c->mergeWith($criteria); - $criteria = $c; - $this->_c = null; - } - } - - /** - * Returns the table alias to be used by the find methods. - * In relational queries, the returned table alias may vary according to - * the corresponding relation declaration. Also, the default table alias - * set by {@link setTableAlias} may be overridden by the applied scopes. - * @param boolean $quote whether to quote the alias name - * @param boolean $checkScopes whether to check if a table alias is defined in the applied scopes so far. - * This parameter must be set false when calling this method in {@link defaultScope}. - * An infinite loop would be formed otherwise. - * @return string the default table alias - */ - public function getTableAlias($quote = false, $checkScopes = true) - { - if ($checkScopes && ($criteria = $this->getDbCriteria(false)) !== null && $criteria->alias != '') - $alias = $criteria->alias; - else - $alias = $this->_alias; - return $quote ? $this->getDbConnection()->getSchema()->quoteTableName($alias) : $alias; - } - - /** - * Sets the table alias to be used in queries. - * @param string $alias the table alias to be used in queries. The alias should NOT be quoted. - */ - public function setTableAlias($alias) - { - $this->_alias = $alias; - } - - /** - * Finds a single active record with the specified condition. - * @param mixed $condition query condition or criteria. - * If a string, it is treated as query condition (the WHERE clause); - * If an array, it is treated as the initial values for constructing a {@link CDbCriteria} object; - * Otherwise, it should be an instance of {@link CDbCriteria}. - * @param array $params parameters to be bound to an SQL statement. - * This is only used when the first parameter is a string (query condition). - * In other cases, please use {@link CDbCriteria::params} to set parameters. - * @return ActiveRecord the record found. Null if no record is found. - */ - public function find($condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.find()', 'system.db.ar.ActiveRecord'); - $criteria = $this->getCommandBuilder()->createCriteria($condition, $params); - return $this->query($criteria); - } - - /** - * Finds all active records satisfying the specified condition. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return array list of active records satisfying the specified condition. An empty array is returned if none is found. - */ - public function findAll($condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.findAll()', 'system.db.ar.ActiveRecord'); - $criteria = $this->getCommandBuilder()->createCriteria($condition, $params); - return $this->query($criteria, true); - } - - /** - * Finds a single active record with the specified primary key. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return ActiveRecord the record found. Null if none is found. - */ - public function findByPk($pk, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.findByPk()', 'system.db.ar.ActiveRecord'); - $prefix = $this->getTableAlias(true) . '.'; - $criteria = $this->getCommandBuilder()->createPkCriteria($this->getTableSchema(), $pk, $condition, $params, $prefix); - return $this->query($criteria); - } - - /** - * Finds all active records with the specified primary keys. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return array the records found. An empty array is returned if none is found. - */ - public function findAllByPk($pk, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.findAllByPk()', 'system.db.ar.ActiveRecord'); - $prefix = $this->getTableAlias(true) . '.'; - $criteria = $this->getCommandBuilder()->createPkCriteria($this->getTableSchema(), $pk, $condition, $params, $prefix); - return $this->query($criteria, true); - } - - /** - * Finds a single active record that has the specified attribute values. - * See {@link find()} for detailed explanation about $condition and $params. - * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. - * Since version 1.0.8, an attribute value can be an array which will be used to generate an IN condition. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return ActiveRecord the record found. Null if none is found. - */ - public function findByAttributes($attributes, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.findByAttributes()', 'system.db.ar.ActiveRecord'); - $prefix = $this->getTableAlias(true) . '.'; - $criteria = $this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(), $attributes, $condition, $params, $prefix); - return $this->query($criteria); - } - - /** - * Finds all active records that have the specified attribute values. - * See {@link find()} for detailed explanation about $condition and $params. - * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. - * Since version 1.0.8, an attribute value can be an array which will be used to generate an IN condition. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return array the records found. An empty array is returned if none is found. - */ - public function findAllByAttributes($attributes, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.findAllByAttributes()', 'system.db.ar.ActiveRecord'); - $prefix = $this->getTableAlias(true) . '.'; - $criteria = $this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(), $attributes, $condition, $params, $prefix); - return $this->query($criteria, true); - } - - /** - * Finds a single active record with the specified SQL statement. - * @param string $sql the SQL statement - * @param array $params parameters to be bound to the SQL statement - * @return ActiveRecord the record found. Null if none is found. - */ - public function findBySql($sql, $params = array()) - { - Yii::trace(get_class($this) . '.findBySql()', 'system.db.ar.ActiveRecord'); - $this->beforeFind(); - if (($criteria = $this->getDbCriteria(false)) !== null && !empty($criteria->with)) - { - $this->_c = null; - $finder = new CActiveFinder($this, $criteria->with); - return $finder->findBySql($sql, $params); + if ($this->_pk === null) { + $this->_pk = $this->getPrimaryKey(); + } + if ($this->updateByPk($this->getOldPrimaryKey(), $values) > 0) { + $this->_pk = $this->getPrimaryKey(); + return true; + } else + { + return false; + } } else { - $command = $this->getCommandBuilder()->createSqlCommand($sql, $params); - return $this->populateRecord($command->queryRow()); + throw new Exception(Yii::t('yii', 'The active record cannot be updated because it is new.')); } } /** - * Finds all active records using the specified SQL statement. - * @param string $sql the SQL statement - * @param array $params parameters to be bound to the SQL statement - * @return array the records found. An empty array is returned if none is found. + * Saves one or several counter columns for the current AR object. + * Note that this method differs from {@link updateCounters} in that it only + * saves the current AR object. + * An example usage is as follows: + *
    +	 * $postRecord=Post::model()->findByPk($postID);
    +	 * $postRecord->saveCounters(array('view_count'=>1));
    +	 * 
    + * Use negative values if you want to decrease the counters. + * @param array $counters the counters to be updated (column name=>increment value) + * @return boolean whether the saving is successful + * @see updateCounters */ - public function findAllBySql($sql, $params = array()) + public function saveCounters($counters) { - Yii::trace(get_class($this) . '.findAllBySql()', 'system.db.ar.ActiveRecord'); - $this->beforeFind(); - if (($criteria = $this->getDbCriteria(false)) !== null && !empty($criteria->with)) - { - $this->_c = null; - $finder = new CActiveFinder($this, $criteria->with); - return $finder->findAllBySql($sql, $params); + Yii::trace(get_class($this) . '.saveCounters()', 'system.db.ar.ActiveRecord'); + $builder = $this->getCommandBuilder(); + $table = $this->getTableSchema(); + $criteria = $builder->createPkCriteria($table, $this->getOldPrimaryKey()); + $command = $builder->createUpdateCounterCommand($this->getTableSchema(), $counters, $criteria); + if ($command->execute()) { + foreach ($counters as $name => $value) + { + $this->$name = $this->$name + $value; + } + return true; } else { - $command = $this->getCommandBuilder()->createSqlCommand($sql, $params); - return $this->populateRecords($command->queryAll()); + return false; } } /** - * Finds the number of rows satisfying the specified query condition. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return string the number of rows satisfying the specified query condition. Note: type is string to keep max. precision. + * Deletes the row corresponding to this active record. + * @return boolean whether the deletion is successful. + * @throws CException if the record is new */ - public function count($condition = '', $params = array()) + public function delete() { - Yii::trace(get_class($this) . '.count()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createCriteria($condition, $params); - $this->applyScopes($criteria); - - if (empty($criteria->with)) - return $builder->createCountCommand($this->getTableSchema(), $criteria)->queryScalar(); - else + if (!$this->getIsNewRecord()) { + Yii::trace(get_class($this) . '.delete()', 'system.db.ar.ActiveRecord'); + if ($this->beforeDelete()) { + $result = $this->deleteByPk($this->getPrimaryKey()) > 0; + $this->afterDelete(); + return $result; + } else + { + return false; + } + } else { - $finder = new CActiveFinder($this, $criteria->with); - return $finder->count($criteria); + throw new Exception(Yii::t('yii', 'The active record cannot be deleted because it is new.')); } } /** - * Finds the number of rows that have the specified attribute values. - * See {@link find()} for detailed explanation about $condition and $params. - * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. - * An attribute value can be an array which will be used to generate an IN condition. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return string the number of rows satisfying the specified query condition. Note: type is string to keep max. precision. + * Repopulates this active record with the latest data. + * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record. */ - public function countByAttributes($attributes, $condition = '', $params = array()) + public function refresh() { - Yii::trace(get_class($this) . '.countByAttributes()', 'system.db.ar.ActiveRecord'); - $prefix = $this->getTableAlias(true) . '.'; - $builder = $this->getCommandBuilder(); - $criteria = $builder->createColumnCriteria($this->getTableSchema(), $attributes, $condition, $params, $prefix); - $this->applyScopes($criteria); - - if (empty($criteria->with)) - return $builder->createCountCommand($this->getTableSchema(), $criteria)->queryScalar(); - else + Yii::trace(get_class($this) . '.refresh()', 'system.db.ar.ActiveRecord'); + if (!$this->getIsNewRecord() && ($record = $this->findByPk($this->getPrimaryKey())) !== null) { + $this->_attributes = array(); + $this->_related = array(); + foreach ($this->getMetaData()->columns as $name => $column) + { + if (property_exists($this, $name)) { + $this->$name = $record->$name; + } + else + { + $this->_attributes[$name] = $record->$name; + } + } + return true; + } else { - $finder = new CActiveFinder($this, $criteria->with); - return $finder->count($criteria); + return false; } } /** - * Finds the number of rows using the given SQL statement. - * This is equivalent to calling {@link CDbCommand::queryScalar} with the specified - * SQL statement and the parameters. - * @param string $sql the SQL statement - * @param array $params parameters to be bound to the SQL statement - * @return string the number of rows using the given SQL statement. Note: type is string to keep max. precision. + * Compares current active record with another one. + * The comparison is made by comparing table name and the primary key values of the two active records. + * @param ActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the database table. */ - public function countBySql($sql, $params = array()) + public function equals($record) { - Yii::trace(get_class($this) . '.countBySql()', 'system.db.ar.ActiveRecord'); - return $this->getCommandBuilder()->createSqlCommand($sql, $params)->queryScalar(); + return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); } /** - * Checks whether there is row satisfying the specified condition. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return boolean whether there is row satisfying the specified condition. + * Returns the primary key value. + * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. */ - public function exists($condition = '', $params = array()) + public function getPrimaryKey() { - Yii::trace(get_class($this) . '.exists()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createCriteria($condition, $params); - $table = $this->getTableSchema(); - $criteria->select = '1'; - $criteria->limit = 1; - $this->applyScopes($criteria); - - if (empty($criteria->with)) - return $builder->createFindCommand($table, $criteria)->queryRow() !== false; - else + $table = static::getMetaData()->table; + if (is_string($table->primaryKey)) { + return $this->{$table->primaryKey}; + } + elseif (is_array($table->primaryKey)) + { + $values = array(); + foreach ($table->primaryKey as $name) + { + $values[$name] = $this->$name; + } + return $values; + } else { - $criteria->select = '*'; - $finder = new CActiveFinder($this, $criteria->with); - return $finder->count($criteria) > 0; + return null; } } /** - * Specifies which related objects should be eagerly loaded. - * This method takes variable number of parameters. Each parameter specifies - * the name of a relation or child-relation. For example, - *
    -	 * // find all posts together with their author and comments
    -	 * Post::model()->with('author','comments')->findAll();
    -	 * // find all posts together with their author and the author's profile
    -	 * Post::model()->with('author','author.profile')->findAll();
    -	 * 
    - * The relations should be declared in {@link relations()}. - * - * By default, the options specified in {@link relations()} will be used - * to do relational query. In order to customize the options on the fly, - * we should pass an array parameter to the with() method. The array keys - * are relation names, and the array values are the corresponding query options. - * For example, - *
    -	 * Post::model()->with(array(
    -	 *     'author'=>array('select'=>'id, name'),
    -	 *     'comments'=>array('condition'=>'approved=1', 'order'=>'create_time'),
    -	 * ))->findAll();
    -	 * 
    - * - * Note, the possible parameters to this method have been changed since version 1.0.2. - * Previously, it was not possible to specify on-th-fly query options, - * and child-relations were specified as hierarchical arrays. - * - * @return ActiveRecord the AR object itself. + * Sets the primary key value. + * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}. + * @param mixed $value the new primary key value. If the primary key is composite, the new value + * should be provided as an array (column name=>column value). */ - public function with() + public function setPrimaryKey($value) { - if (func_num_args() > 0) + $this->_pk = $this->getPrimaryKey(); + $table = $this->getMetaData()->tableSchema; + if (is_string($table->primaryKey)) { + $this->{$table->primaryKey} = $value; + } + elseif (is_array($table->primaryKey)) { - $with = func_get_args(); - if (is_array($with[0])) // the parameter is given as an array - $with = $with[0]; - if (!empty($with)) - $this->getDbCriteria()->mergeWith(array('with' => $with)); + foreach ($table->primaryKey as $name) + { + $this->$name = $value[$name]; + } } - return $this; - } - - /** - * Sets {@link CDbCriteria::together} property to be true. - * This is only used in relational AR query. Please refer to {@link CDbCriteria::together} - * for more details. - * @return ActiveRecord the AR object itself - */ - public function together() - { - $this->getDbCriteria()->together = true; - return $this; - } - - /** - * Updates records with the specified primary key(s). - * See {@link find()} for detailed explanation about $condition and $params. - * Note, the attributes are not checked for safety and validation is NOT performed. - * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). - * @param array $attributes list of attributes (name=>$value) to be updated - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - */ - public function updateByPk($pk, $attributes, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.updateByPk()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $table = $this->getTableSchema(); - $criteria = $builder->createPkCriteria($table, $pk, $condition, $params); - $command = $builder->createUpdateCommand($table, $attributes, $criteria); - return $command->execute(); - } - - /** - * Updates records with the specified condition. - * See {@link find()} for detailed explanation about $condition and $params. - * Note, the attributes are not checked for safety and no validation is done. - * @param array $attributes list of attributes (name=>$value) to be updated - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - */ - public function updateAll($attributes, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.updateAll()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createCriteria($condition, $params); - $command = $builder->createUpdateCommand($this->getTableSchema(), $attributes, $criteria); - return $command->execute(); - } - - /** - * Updates one or several counter columns. - * Note, this updates all rows of data unless a condition or criteria is specified. - * See {@link find()} for detailed explanation about $condition and $params. - * @param array $counters the counters to be updated (column name=>increment value) - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - * @see saveCounters - */ - public function updateCounters($counters, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.updateCounters()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createCriteria($condition, $params); - $command = $builder->createUpdateCounterCommand($this->getTableSchema(), $counters, $criteria); - return $command->execute(); - } - - /** - * Deletes rows with the specified primary key. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer the number of rows deleted - */ - public function deleteByPk($pk, $condition = '', $params = array()) - { - Yii::trace(get_class($this) . '.deleteByPk()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createPkCriteria($this->getTableSchema(), $pk, $condition, $params); - $command = $builder->createDeleteCommand($this->getTableSchema(), $criteria); - return $command->execute(); } /** - * Deletes rows with the specified condition. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer the number of rows deleted + * Returns the old primary key value. + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. */ - public function deleteAll($condition = '', $params = array()) + public function getOldPrimaryKey() { - Yii::trace(get_class($this) . '.deleteAll()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $criteria = $builder->createCriteria($condition, $params); - $command = $builder->createDeleteCommand($this->getTableSchema(), $criteria); - return $command->execute(); + return $this->_pk; } /** - * Deletes rows which match the specified attribute values. - * See {@link find()} for detailed explanation about $condition and $params. - * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. - * Since version 1.0.8, an attribute value can be an array which will be used to generate an IN condition. - * @param mixed $condition query condition or criteria. - * @param array $params parameters to be bound to an SQL statement. - * @return integer number of rows affected by the execution. + * Sets the old primary key value. + * @param mixed $value the old primary key value. */ - public function deleteAllByAttributes($attributes, $condition = '', $params = array()) + public function setOldPrimaryKey($value) { - Yii::trace(get_class($this) . '.deleteAllByAttributes()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $table = $this->getTableSchema(); - $criteria = $builder->createColumnCriteria($table, $attributes, $condition, $params); - $command = $builder->createDeleteCommand($table, $criteria); - return $command->execute(); + $this->_pk = $value; } /** @@ -1755,54 +1152,20 @@ abstract class ActiveRecord extends \yii\base\Model * @return ActiveRecord the newly created active record. The class of the object is the same as the model class. * Null is returned if the input data is false. */ - public function populateRecord($attributes, $callAfterFind = true) - { - if ($attributes !== false) - { - $record = $this->instantiate($attributes); - $record->setScenario('update'); - $record->init(); - $md = $record->getMetaData(); - foreach ($attributes as $name => $value) - { - if (property_exists($record, $name)) - $record->$name = $value; - elseif (isset($md->columns[$name])) - $record->_attributes[$name] = $value; - } - $record->_pk = $record->getPrimaryKey(); - $record->attachBehaviors($record->behaviors()); - if ($callAfterFind) - $record->afterFind(); - return $record; - } else - return null; - } - - /** - * Creates a list of active records based on the input data. - * This method is internally used by the find methods. - * @param array $data list of attribute values for the active records. - * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated. - * This parameter is added in version 1.0.3. - * @param string $index the name of the attribute whose value will be used as indexes of the query result array. - * If null, it means the array will be indexed by zero-based integers. - * @return array list of active records. - */ - public function populateRecords($data, $callAfterFind = true, $index = null) + public static function populateRecord($row) { - $records = array(); - foreach ($data as $attributes) - { - if (($record = $this->populateRecord($attributes, $callAfterFind)) !== null) - { - if ($index === null) - $records[] = $record; - else - $records[$record->$index] = $record; + $record = static::instantiate($row); + $record->setScenario('update'); + $columns = static::getMetaData()->table->columns; + foreach ($row as $name => $value) { + if (property_exists($record, $name)) { + $record->$name = $value; + } elseif (isset($columns[$name])) { + $record->_attributes[$name] = $value; } } - return $records; + $record->_pk = $record->getPrimaryKey(); + return $record; } /** @@ -1815,11 +1178,9 @@ abstract class ActiveRecord extends \yii\base\Model * @param array $attributes list of attribute values for the active records. * @return ActiveRecord the active record */ - protected function instantiate($attributes) + protected static function instantiate($row) { - $class = get_class($this); - $model = new $class(null); - return $model; + return static::newInstance(); } /** @@ -1839,7 +1200,7 @@ abstract class ActiveRecord extends \yii\base\Model * CBaseActiveRelation is the base class for all active relations. * @author Qiang Xue */ -class CBaseActiveRelation extends CComponent +class CBaseActiveRelation extends \yii\base\Component { /** * @var string name of the related object @@ -1903,7 +1264,9 @@ class CBaseActiveRelation extends CComponent $this->className = $className; $this->foreignKey = $foreignKey; foreach ($options as $name => $value) + { $this->$name = $value; + } } /** @@ -1913,12 +1276,13 @@ class CBaseActiveRelation extends CComponent */ public function mergeWith($criteria, $fromScope = false) { - if ($criteria instanceof CDbCriteria) + if ($criteria instanceof CDbCriteria) { $criteria = $criteria->toArray(); - if (isset($criteria['select']) && $this->select !== $criteria['select']) - { - if ($this->select === '*') + } + if (isset($criteria['select']) && $this->select !== $criteria['select']) { + if ($this->select === '*') { $this->select = $criteria['select']; + } elseif ($criteria['select'] !== '*') { $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; @@ -1927,47 +1291,58 @@ class CBaseActiveRelation extends CComponent } } - if (isset($criteria['condition']) && $this->condition !== $criteria['condition']) - { - if ($this->condition === '') + if (isset($criteria['condition']) && $this->condition !== $criteria['condition']) { + if ($this->condition === '') { $this->condition = $criteria['condition']; + } elseif ($criteria['condition'] !== '') + { $this->condition = "( {$this->condition}) AND ( {$criteria['condition']})"; + } } - if (isset($criteria['params']) && $this->params !== $criteria['params']) + if (isset($criteria['params']) && $this->params !== $criteria['params']) { $this->params = array_merge($this->params, $criteria['params']); + } - if (isset($criteria['order']) && $this->order !== $criteria['order']) - { - if ($this->order === '') + if (isset($criteria['order']) && $this->order !== $criteria['order']) { + if ($this->order === '') { $this->order = $criteria['order']; + } elseif ($criteria['order'] !== '') + { $this->order = $criteria['order'] . ', ' . $this->order; + } } - if (isset($criteria['group']) && $this->group !== $criteria['group']) - { - if ($this->group === '') + if (isset($criteria['group']) && $this->group !== $criteria['group']) { + if ($this->group === '') { $this->group = $criteria['group']; + } elseif ($criteria['group'] !== '') + { $this->group .= ', ' . $criteria['group']; + } } - if (isset($criteria['join']) && $this->join !== $criteria['join']) - { - if ($this->join === '') + if (isset($criteria['join']) && $this->join !== $criteria['join']) { + if ($this->join === '') { $this->join = $criteria['join']; + } elseif ($criteria['join'] !== '') + { $this->join .= ' ' . $criteria['join']; + } } - if (isset($criteria['having']) && $this->having !== $criteria['having']) - { - if ($this->having === '') + if (isset($criteria['having']) && $this->having !== $criteria['having']) { + if ($this->having === '') { $this->having = $criteria['having']; + } elseif ($criteria['having'] !== '') + { $this->having = "( {$this->having}) AND ( {$criteria['having']})"; + } } } } @@ -1997,12 +1372,14 @@ class CStatRelation extends CBaseActiveRelation */ public function mergeWith($criteria, $fromScope = false) { - if ($criteria instanceof CDbCriteria) + if ($criteria instanceof CDbCriteria) { $criteria = $criteria->toArray(); + } parent::mergeWith($criteria, $fromScope); - if (isset($criteria['defaultValue'])) + if (isset($criteria['defaultValue'])) { $this->defaultValue = $criteria['defaultValue']; + } } } @@ -2050,7 +1427,7 @@ class CActiveRelation extends CBaseActiveRelation *
  • Multiple scopes: 'scopes'=>array('scopeName1','scopeName2').
  • * */ - public $scopes; + public $scopes; /** * Merges this relation with a criteria specified dynamically. @@ -2059,41 +1436,49 @@ class CActiveRelation extends CBaseActiveRelation */ public function mergeWith($criteria, $fromScope = false) { - if ($criteria instanceof CDbCriteria) + if ($criteria instanceof CDbCriteria) { $criteria = $criteria->toArray(); - if ($fromScope) - { - if (isset($criteria['condition']) && $this->on !== $criteria['condition']) - { - if ($this->on === '') + } + if ($fromScope) { + if (isset($criteria['condition']) && $this->on !== $criteria['condition']) { + if ($this->on === '') { $this->on = $criteria['condition']; + } elseif ($criteria['condition'] !== '') + { $this->on = "( {$this->on}) AND ( {$criteria['condition']})"; + } } unset($criteria['condition']); } parent::mergeWith($criteria); - if (isset($criteria['joinType'])) + if (isset($criteria['joinType'])) { $this->joinType = $criteria['joinType']; + } - if (isset($criteria['on']) && $this->on !== $criteria['on']) - { - if ($this->on === '') + if (isset($criteria['on']) && $this->on !== $criteria['on']) { + if ($this->on === '') { $this->on = $criteria['on']; + } elseif ($criteria['on'] !== '') + { $this->on = "( {$this->on}) AND ( {$criteria['on']})"; + } } - if (isset($criteria['with'])) + if (isset($criteria['with'])) { $this->with = $criteria['with']; + } - if (isset($criteria['alias'])) + if (isset($criteria['alias'])) { $this->alias = $criteria['alias']; + } - if (isset($criteria['together'])) + if (isset($criteria['together'])) { $this->together = $criteria['together']; + } } } @@ -2156,17 +1541,21 @@ class CHasManyRelation extends CActiveRelation */ public function mergeWith($criteria, $fromScope = false) { - if ($criteria instanceof CDbCriteria) + if ($criteria instanceof CDbCriteria) { $criteria = $criteria->toArray(); + } parent::mergeWith($criteria, $fromScope); - if (isset($criteria['limit']) && $criteria['limit'] > 0) + if (isset($criteria['limit']) && $criteria['limit'] > 0) { $this->limit = $criteria['limit']; + } - if (isset($criteria['offset']) && $criteria['offset'] >= 0) + if (isset($criteria['offset']) && $criteria['offset'] >= 0) { $this->offset = $criteria['offset']; + } - if (isset($criteria['index'])) + if (isset($criteria['index'])) { $this->index = $criteria['index']; + } } } @@ -2182,69 +1571,44 @@ class CManyManyRelation extends CHasManyRelation /** - * ActiveRecordMetaData represents the meta-data for an Active Record class. + * ActiveMetaData represents the meta-data for an Active Record class. * * @author Qiang Xue * @since 2.0 */ -class ActiveRecordMetaData +class ActiveMetaData { /** - * @var CDbTableSchema the table schema information - */ - public $tableSchema; - /** - * @var array table columns + * @var TableSchema the table schema information */ - public $columns; + public $table; /** * @var array list of relations */ public $relations = array(); - /** - * @var array attribute default values - */ - public $attributeDefaults = array(); - - private $_model; /** * Constructor. - * @param ActiveRecord $model the model instance + * @param string $modelClass the model class name */ - public function __construct($model) + public function __construct($modelClass) { - $this->_model = $model; - - $tableName = $model->tableName(); - if (($table = $model->getDbConnection()->getSchema()->getTable($tableName)) === null) - throw new CDbException(Yii::t('yii', 'The table "{table}" for active record class "{class}" cannot be found in the database.', - array('{class}' => get_class($model), '{table}' => $tableName))); - if ($table->primaryKey === null) - { - $table->primaryKey = $model->primaryKey(); - if (is_string($table->primaryKey) && isset($table->columns[$table->primaryKey])) - $table->columns[$table->primaryKey]->isPrimaryKey = true; - elseif (is_array($table->primaryKey)) - { - foreach ($table->primaryKey as $name) - { - if (isset($table->columns[$name])) - $table->columns[$name]->isPrimaryKey = true; - } - } + $tableName = $modelClass::tableName(); + $table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); + if ($table === null) { + throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); } - $this->tableSchema = $table; - $this->columns = $table->columns; - - foreach ($table->columns as $name => $column) - { - if (!$column->isPrimaryKey && $column->defaultValue !== null) - $this->attributeDefaults[$name] = $column->defaultValue; + if ($table->primaryKey === null) { + $primaryKey = $modelClass::primaryKey(); + if ($primaryKey !== null) { + $table->fixPrimaryKey($primaryKey); + } else { + throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key."); + } } + $this->table = $table; - foreach ($model->relations() as $name => $config) - { + foreach ($modelClass::relations() as $name => $config) { $this->addRelation($name, $config); } } @@ -2255,38 +1619,18 @@ class ActiveRecordMetaData * $config is an array with three elements: * relation type, the related active record class and the foreign key. * - * @throws CDbException + * @throws Exception * @param string $name $name Name of the relation. * @param array $config $config Relation parameters. - * @return void + * @return void */ public function addRelation($name, $config) { - if (isset($config[0], $config[1], $config[2])) // relation class, AR class, FK + // relation class, AR class, FK + if (isset($config[0], $config[1], $config[2])) { $this->relations[$name] = new $config[0]($name, $config[1], $config[2], array_slice($config, 3)); - else - throw new CDbException(Yii::t('yii', 'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.', array('{class}' => get_class($this->_model), '{relation}' => $name))); - } - - /** - * Checks if there is a relation with specified name defined. - * - * @param string $name $name Name of the relation. - * @return boolean - */ - public function hasRelation($name) - { - return isset($this->relations[$name]); - } - - /** - * Deletes a relation with specified name. - * - * @param string $name $name - * @return void - */ - public function removeRelation($name) - { - unset($this->relations[$name]); + } else { + throw new Exception(Yii::t('yii', 'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.', array('{class}' => get_class($this->_model), '{relation}' => $name))); + } } } diff --git a/framework/db/dao/TableSchema.php b/framework/db/dao/TableSchema.php index 1ed2420..39a8dd9 100644 --- a/framework/db/dao/TableSchema.php +++ b/framework/db/dao/TableSchema.php @@ -10,6 +10,8 @@ namespace yii\db\dao; +use yii\db\Exception; + /** * TableSchema represents the metadata of a database table. * @@ -82,4 +84,18 @@ class TableSchema extends \yii\base\Object { return array_keys($this->columns); } + + public function fixPrimaryKey($keys) + { + if (!is_array($keys)) { + $keys = array($keys); + } + foreach ($keys as $key) { + if (isset($this->columns[$key])) { + $this->columns[$key]->isPrimaryKey = true; + } else { + throw new Exception("Primary key '$key' cannot be found in table '{$this->name}'."); + } + } + } } diff --git a/framework/util/Text.php b/framework/util/Text.php index 582775f..c41a6e2 100644 --- a/framework/util/Text.php +++ b/framework/util/Text.php @@ -14,6 +14,8 @@ namespace yii\util; /** * Text helper * + * @author Qiang Xue + * @author Alex Makarov * @since 2.0 */ class Text @@ -23,9 +25,9 @@ class Text * @param string $name the word to be pluralized * @return string the pluralized word */ - public function pluralize($name) + public static function pluralize($name) { - $rules=array( + $rules = array( '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', '/(m)an$/i' => '\1en', @@ -33,11 +35,17 @@ class Text '/(r)y$/i' => '\1ies', '/s$/' => 's', ); - foreach($rules as $rule=>$replacement) + foreach ($rules as $rule => $replacement) { - if(preg_match($rule,$name)) - return preg_replace($rule,$replacement,$name); + if (preg_match($rule, $name)) { + return preg_replace($rule, $replacement, $name); + } } - return $name.'s'; + return $name . 's'; + } + + public static function dd($value) + { + return trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(?$this->item3,'key4'=>$this->item1); - $this->dictionary->fromArray($array); + $this->dictionary->copyFrom($array); $this->assertEquals(2, $this->dictionary->getCount()); $this->assertEquals($this->item3, $this->dictionary['key3']); $this->assertEquals($this->item1, $this->dictionary['key4']); $this->setExpectedException('yii\base\Exception'); - $this->dictionary->fromArray($this); + $this->dictionary->copyFrom($this); } public function testMergeWith() diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index 9321c98..2752f49 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -117,7 +117,6 @@ class ObjectTest extends \yiiunit\TestCase $this->assertTrue(empty($this->object->Text)); } - public function testEvaluateExpression() { $object = new NewObject; diff --git a/tests/unit/framework/base/VectorTest.php b/tests/unit/framework/base/VectorTest.php index 7eeba33..66cbfd3 100644 --- a/tests/unit/framework/base/VectorTest.php +++ b/tests/unit/framework/base/VectorTest.php @@ -113,10 +113,10 @@ class VectorTest extends \yiiunit\TestCase public function testFromArray() { $array=array($this->item3,$this->item1); - $this->vector->fromArray($array); + $this->vector->copyFrom($array); $this->assertTrue(count($array)==2 && $this->vector[0]===$this->item3 && $this->vector[1]===$this->item1); $this->setExpectedException('yii\base\Exception'); - $this->vector->fromArray($this); + $this->vector->copyFrom($this); } public function testMergeWith() diff --git a/tests/unit/framework/db/dao/CommandTest.php b/tests/unit/framework/db/dao/CommandTest.php index 74948c0..4971311 100644 --- a/tests/unit/framework/db/dao/CommandTest.php +++ b/tests/unit/framework/db/dao/CommandTest.php @@ -18,33 +18,33 @@ class CommandTest extends \yiiunit\MysqlTestCase $this->assertEquals(null, $command->sql); // string - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $this->assertEquals($sql, $command->sql); // Query object $query = new Query; - $query->select('id')->from('tbl_user'); + $query->select('id')->from('tbl_customer'); $command = $db->createCommand($query); - $this->assertEquals("SELECT `id` FROM `tbl_user`", $command->sql); + $this->assertEquals("SELECT `id` FROM `tbl_customer`", $command->sql); // array $command = $db->createCommand(array( 'select' => 'name', - 'from' => 'tbl_user', + 'from' => 'tbl_customer', )); - $this->assertEquals("SELECT `name` FROM `tbl_user`", $command->sql); + $this->assertEquals("SELECT `name` FROM `tbl_customer`", $command->sql); } function testGetSetSql() { $db = $this->getConnection(false); - $sql = 'SELECT * FROM yii_user'; + $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $this->assertEquals($sql, $command->sql); - $sql2 = 'SELECT * FROM yii_yii_post'; + $sql2 = 'SELECT * FROM tbl_order'; $command->sql = $sql2; $this->assertEquals($sql2, $command->sql); } @@ -53,7 +53,7 @@ class CommandTest extends \yiiunit\MysqlTestCase { $db = $this->getConnection(false); - $command = $db->createCommand('SELECT * FROM yii_user'); + $command = $db->createCommand('SELECT * FROM tbl_customer'); $this->assertEquals(null, $command->pdoStatement); $command->prepare(); $this->assertNotEquals(null, $command->pdoStatement); @@ -65,11 +65,11 @@ class CommandTest extends \yiiunit\MysqlTestCase { $db = $this->getConnection(); - $sql = 'INSERT INTO yii_comment(content,post_id,author_id) VALUES (\'test comment\', 1, 1)'; + $sql = 'INSERT INTO tbl_customer(email, name , address) VALUES (\'user4@example.com\', \'user4\', \'address4\')'; $command = $db->createCommand($sql); $this->assertEquals(1, $command->execute()); - $sql = 'SELECT COUNT(*) FROM yii_comment WHERE content=\'test comment\''; + $sql = 'SELECT COUNT(*) FROM tbl_customer WHERE name =\'user4\''; $command = $db->createCommand($sql); $this->assertEquals(1, $command->queryScalar()); @@ -83,55 +83,55 @@ class CommandTest extends \yiiunit\MysqlTestCase $db = $this->getConnection(); // query - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $reader = $db->createCommand($sql)->query(); $this->assertTrue($reader instanceof DataReader); // queryAll - $rows = $db->createCommand('SELECT * FROM yii_post')->queryAll(); - $this->assertEquals(5, count($rows)); + $rows = $db->createCommand('SELECT * FROM tbl_customer')->queryAll(); + $this->assertEquals(3, count($rows)); $row = $rows[2]; $this->assertEquals(3, $row['id']); - $this->assertEquals($row['title'], 'post 3'); + $this->assertEquals('user3', $row['name']); - $rows = $db->createCommand('SELECT * FROM yii_post WHERE id=10')->queryAll(); + $rows = $db->createCommand('SELECT * FROM tbl_customer WHERE id=10')->queryAll(); $this->assertEquals(array(), $rows); // queryRow - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; $row = $db->createCommand($sql)->queryRow(); $this->assertEquals(1, $row['id']); - $this->assertEquals('post 1', $row['title'], 'post 1'); + $this->assertEquals('user1', $row['name']); - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; $command = $db->createCommand($sql); $command->prepare(); $row = $command->queryRow(); $this->assertEquals(1, $row['id']); - $this->assertEquals('post 1', $row['title']); + $this->assertEquals('user1', $row['name']); - $sql = 'SELECT * FROM yii_post WHERE id=10'; + $sql = 'SELECT * FROM tbl_customer WHERE id=10'; $command = $db->createCommand($sql); $this->assertFalse($command->queryRow()); // queryColumn - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $column = $db->createCommand($sql)->queryColumn(); - $this->assertEquals(range(1, 5), $column); + $this->assertEquals(range(1, 3), $column); - $command = $db->createCommand('SELECT id FROM yii_post WHERE id=10'); + $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); $this->assertEquals(array(), $command->queryColumn()); // queryScalar - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; $this->assertEquals($db->createCommand($sql)->queryScalar(), 1); - $sql = 'SELECT id FROM yii_post'; + $sql = 'SELECT id FROM tbl_customer ORDER BY id'; $command = $db->createCommand($sql); $command->prepare(); $this->assertEquals(1, $command->queryScalar()); - $command = $db->createCommand('SELECT id FROM yii_post WHERE id=10'); + $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); $this->assertFalse($command->queryScalar()); $command = $db->createCommand('bad SQL'); @@ -144,20 +144,22 @@ class CommandTest extends \yiiunit\MysqlTestCase $db = $this->getConnection(); // bindParam - $sql = 'INSERT INTO yii_post(title,create_time,author_id) VALUES (:title, :create_time, 1)'; - $command = $db->createCommand($sql); - $title = 'test title'; - $createTime = time(); - $command->bindParam(':title', $title); - $command->bindParam(':create_time', $createTime); + $sql = 'INSERT INTO tbl_customer(email,name,address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); $command->execute(); - $sql = 'SELECT create_time FROM yii_post WHERE title=:title'; + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; $command = $db->createCommand($sql); - $command->bindParam(':title', $title); - $this->assertEquals($createTime, $command->queryScalar()); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); - $sql = 'INSERT INTO yii_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, :blob_col, :numeric_col, :bool_col)'; + $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, :blob_col, :numeric_col, :bool_col)'; $command = $db->createCommand($sql); $intCol = 123; $charCol = 'abc'; @@ -173,7 +175,7 @@ class CommandTest extends \yiiunit\MysqlTestCase $command->bindParam(':bool_col', $boolCol); $this->assertEquals(1, $command->execute()); - $sql = 'SELECT * FROM yii_type'; + $sql = 'SELECT * FROM tbl_type'; $row = $db->createCommand($sql)->queryRow(); $this->assertEquals($intCol, $row['int_col']); $this->assertEquals($charCol, $row['char_col']); @@ -182,23 +184,23 @@ class CommandTest extends \yiiunit\MysqlTestCase $this->assertEquals($numericCol, $row['numeric_col']); // bindValue - $sql = 'INSERT INTO yii_comment(content,post_id,author_id) VALUES (:content, 1, 1)'; + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; $command = $db->createCommand($sql); - $command->bindValue(':content', 'test comment'); + $command->bindValue(':email', 'user5@example.com'); $command->execute(); - $sql = 'SELECT post_id FROM yii_comment WHERE content=:content'; + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; $command = $db->createCommand($sql); - $command->bindValue(':content', 'test comment'); - $this->assertEquals(1, $command->queryScalar()); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); // bind value via query or execute method - $sql = 'INSERT INTO yii_comment(content,post_id,author_id) VALUES (:content, 1, 1)'; + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user6\', \'address6\')'; $command = $db->createCommand($sql); - $command->execute(array(':content' => 'test comment2')); - $sql = 'SELECT post_id FROM yii_comment WHERE content=:content'; + $command->execute(array(':email' => 'user6@example.com')); + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; $command = $db->createCommand($sql); - $this->assertEquals(1, $command->queryScalar(array(':content' => 'test comment2'))); + $this->assertEquals('user5@example.com', $command->queryScalar(array(':name' => 'user5'))); } function testFetchMode() @@ -206,20 +208,20 @@ class CommandTest extends \yiiunit\MysqlTestCase $db = $this->getConnection(); // default: FETCH_ASSOC - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $result = $command->queryRow(); $this->assertTrue(is_array($result) && isset($result['id'])); // FETCH_OBJ, customized via fetchMode property - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $command->fetchMode = \PDO::FETCH_OBJ; $result = $command->queryRow(); $this->assertTrue(is_object($result)); // FETCH_NUM, customized in query method - $sql = 'SELECT * FROM yii_post'; + $sql = 'SELECT * FROM tbl_customer'; $command = $db->createCommand($sql); $result = $command->queryRow(array(), \PDO::FETCH_NUM); $this->assertTrue(is_array($result) && isset($result[0]));