From f1c27f46100582a1e52a27b616bf468e849068e7 Mon Sep 17 00:00:00 2001 From: rojaro <> Date: Wed, 15 Jul 2009 07:58:26 +0000 Subject: moved System.Db to System.Testing.Data and updated classes accordingly --- .../Testing/Data/ActiveRecord/TActiveRecord.php | 1843 ++++++++++++++++++++ 1 file changed, 1843 insertions(+) create mode 100644 framework/Testing/Data/ActiveRecord/TActiveRecord.php (limited to 'framework/Testing/Data/ActiveRecord/TActiveRecord.php') diff --git a/framework/Testing/Data/ActiveRecord/TActiveRecord.php b/framework/Testing/Data/ActiveRecord/TActiveRecord.php new file mode 100644 index 00000000..8d6c5e10 --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/TActiveRecord.php @@ -0,0 +1,1843 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CActiveRecord 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 + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ + +Prado::using('System.Base.TModel'); +Prado::using('System.Testing.Data.*'); +Prado::using('System.Testing.Data.ActiveRecord.*'); +Prado::using('System.Base.TEvent'); +Prado::using('System.Testing.Data.ActiveRecord.TActiveRecordCriteria'); + +abstract class TActiveRecord extends TModel +{ + const BELONGS_TO='TBelongsToRelation'; + const HAS_ONE='THasOneRelation'; + const HAS_MANY='THasManyRelation'; + const MANY_MANY='TManyManyRelation'; + const STAT='TStatRelation'; + + /** + * @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) + + /** + * Returns the instance of a active record finder for a particular class. + * The finder objects are static instances for each ActiveRecord class. + * This means that event handlers bound to these finder instances are class wide. + * Create a new instance of the ActiveRecord class if you wish to bound the + * event handlers to object instance. + * @param string active record class name. + * @return TActiveRecord active record finder instance. + */ + + /** + * Constructor. + * @param string scenario name. See {@link CModel::scenario} for more details about this parameter. + */ + public function __construct($scenario='insert') + { + if($scenario===null) // internally used by populateRecord() and model() + return; + + $this->setScenario($scenario); + $this->setIsNewRecord(true); + $this->_attributes=$this->getMetaData()->attributeDefaults; + + $this->attachBehaviors($this->behaviors()); + $this->afterConstruct(); + } + + /** + * PHP sleep magic method. + * This method ensures that the model meta data reference is set to null. + */ + public function __sleep() + { + $this->_md=null; + return array_keys((array)$this); + } + + /** + * PHP getter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string property name + * @return mixed property value + * @see getAttribute + */ + public function __get($name) + { + if(isset($this->_attributes[$name])) + return $this->_attributes[$name]; + else if(isset($this->getMetaData()->columns[$name])) + return null; + else if(isset($this->_related[$name])) + return $this->_related[$name]; + else if(isset($this->getMetaData()->relations[$name])) + return $this->getRelated($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 property name + * @param mixed property value + */ + public function __set($name,$value) + { + if($this->setAttribute($name,$value)===false) + 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 the property name or the event name + * @return boolean whether the property value is null + * @since 1.0.1 + */ + public function __isset($name) + { + if(isset($this->_attributes[$name])) + return true; + else if(isset($this->getMetaData()->columns[$name])) + return false; + else if(isset($this->_related[$name])) + return true; + else if(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 the property name or the event name + * @since 1.0.1 + */ + public function __unset($name) + { + if(isset($this->getMetaData()->columns[$name])) + unset($this->_attributes[$name]); + else if(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 the method name + * @param array method parameters + * @return mixed the method return value + * @since 1.0.5 + * + * @TODO: implement __call to parent + * + */ + public function __call($name,$parameters) + { + 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 the relation name (see {@link relations}) + * @param boolean whether to reload the related objects from database. Defaults to false. + * @param array 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}. + * @since 1.0.2 + */ + public function getRelated($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 TDbException(get_class($this).' does not have relation "'.$name.'".'); + + Prado::trace('lazy loading '.get_class($this).'.'.$name,'System.Testing.Data.ActiveRecord.TActiveRecord'); + $relation=$md->relations[$name]; + if($this->getIsNewRecord() && ($relation instanceof THasOneRelation || $relation instanceof THasManyRelation)) + return $relation instanceof THasOneRelation ? null : array(); + + if($params!==array()) // dynamic query + { + $exists=isset($this->_related[$name]) || array_key_exists($name,$this->_related); + if($exists) + $save=$this->_related[$name]; + unset($this->_related[$name]); + $r=array($name=>$params); + } + else + $r=$name; + + $finder=new TActiveFinder($this,$r); + $finder->lazyFind($this); + + if(!isset($this->_related[$name])) + { + if($relation instanceof THasManyRelation) + $this->_related[$name]=array(); + else if($relation instanceof TStatRelation) + $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 the relation name + * @return booolean a value indicating whether the named related object(s) has been loaded. + * @since 1.0.3 + */ + public function hasRelated($name) + { + return isset($this->_related[$name]) || array_key_exists($name,$this->_related); + } + + /** + * Returns the query criteria associated with this model. + * @param boolean 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. + * @since 1.0.5 + */ + public function getDbCriteria($createIfNull=true) + { + if($this->_c===null) + { + if(($c=$this->defaultScope())!==array() || $createIfNull) + $this->_c=new TDbCriteria($c); + } + return $this->_c; + } + + /** + * Returns the default named scope that should be applied to all queries implicitly for this model. + * The default implementation simply returns an empty array. You may override this method + * if the model needs to be queried with some default criteria (e.g. only active records should be returned). + * @return array the query criteria. This will be used as the parameter to the constructor + * of {@link CDbCriteria}. + * @since 1.0.5 + */ + public function defaultScope() + { + return array(); + } + + /** + * 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 active record class name. + * @return CActiveRecord 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->attachBehaviors($model->behaviors()); + $model->_md=new TActiveRecordMetaData($model); + return $model; + } + } + + /** + * @return CActiveRecordMetaData 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; + } + + /** + * 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() + { + return get_class($this); + } + + /** + * 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, + * 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. + * If the key is a single column, it should return the column name; + * If the key is a composite one consisting of several columns, it should + * return the array of the key column names. + * @since 1.0.4 + */ + public function primaryKey() + { + } + + /** + * This method should be overridden to declare related objects. + * + * There are four types of relations that may exist between two active record objects: + * + * + * By declaring these relations, CActiveRecord can bring back related objects + * in either lazy loading or eager loading approach, and you save the effort of + * writing complex JOIN SQL statements. + * + * Each kind of related objects is defined in this method as an array with the following elements: + *
+	 * 'varName'=>array('relationType', 'className', 'foreignKey', ...additional options)
+	 * 
+ * where 'varName' refers to the name of the variable/property that the related object(s) can + * be accessed through; 'relationType' refers to the type of the relation, which can be one of the + * following four constants: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY; + * 'className' refers to the name of the active record class that the related object(s) is of; + * and 'foreignKey' states the foreign key that relates the two kinds of active record. + * Note, for composite foreign keys, they must be listed together, separating with space or comma; + * and for foreign keys used in MANY_MANY relation, the joining table must be declared as well + * (e.g. 'joinTable(fk1, fk2)'). + * + * Additional options may be specified as name-value pairs in the rest array elements: + * + * + * The following options are available for certain relations when lazy loading: + * + * + * Below is an example declaring related objects for 'Post' active record class: + *
+	 * return array(
+	 *     'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
+	 *     'comments'=>array(self::HAS_MANY, 'Comment', 'postID', 'with'=>'author', 'order'=>'createTime DESC'),
+	 *     'tags'=>array(self::MANY_MANY, 'Tag', 'PostTag(postID, tagID)', 'order'=>'name'),
+	 * );
+	 * 
+ * + * @return array list of related object declarations. Defaults to empty array. + */ + public function relations() + { + return array(); + } + + /** + * Returns the declaration of named scopes. + * A named scope represents a query criteria that can be chained together with + * other named scopes and applied to a query. This method should be overridden + * by child classes to declare named scopes for the particular AR classes. + * For example, the following code declares two named scopes: 'recently' and + * 'published'. + *
+	 * return array(
+	 *     'published'=>array(
+	 *           'condition'=>'status=1',
+	 *     ),
+	 *     'recently'=>array(
+	 *           'order'=>'createTime DESC',
+	 *           'limit'=>5,
+	 *     ),
+	 * );
+	 * 
+ * If the above scopes are declared in a 'Post' model, we can perform the following + * queries: + *
+	 * $posts=Post::model()->published()->findAll();
+	 * $posts=Post::model()->published()->recently()->findAll();
+	 * $posts=Post::model()->published()->with('comments')->findAll();
+	 * 
+ * Note that the last query is a relational query. + * + * @return array the scope definition. The array keys are scope names; the array + * values are the corresponding scope definitions. Each scope definition is represented + * as an array whose keys must be properties of {@link CDbCriteria}. + * @since 1.0.5 + */ + public 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. + * @since 1.0.1 + */ + public function attributeNames() + { + return array_keys($this->getMetaData()->columns); + } + + /** + * 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. + * + * @TODO Get default db connection if theres none set + * + */ + public function getDbConnection() + { + if(self::$db!==null) + return self::$db; + else + { + self::$db=Prado::getApplication()->getModule($this->getConnectionId())->getDbConnection(); + if(self::$db instanceof TDbConnection) + { + self::$db->setActive(true); + return self::$db; + } + else + throw new TDbException('ActiveRecord requires a db connection.'); + } + } + + /** + * Returns connection id (must be overloaded by child classes) + * @return string Id of TDbConnection module + */ + abstract public function getConnectionId(); + + /** + * @param string the relation name + * @return CActiveRelation the named relation declared for this AR class. Null if the relation does not exist. + */ + public function getActiveRelation($name) + { + return isset($this->getMetaData()->relations[$name]) ? $this->getMetaData()->relations[$name] : null; + } + + /** + * @return CDbTableSchema the metadata of the table that this AR belongs to + */ + public function getTableSchema() + { + return $this->getMetaData()->tableSchema; + } + + /** + * @return CDbCommandBuilder the command builder used by this AR + */ + public function getCommandBuilder() + { + return $this->getDbConnection()->getSchema()->getCommandBuilder(); + } + + /** + * @param string attribute name + * @return boolean whether this AR has the named attribute (table column). + */ + public function hasAttribute($name) + { + return isset($this->getMetaData()->columns[$name]); + } + + /** + * Returns the named attribute value. + * If this is a new record and the attribute is not set before, + * the default column value will be returned. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * You may also use $this->AttributeName to obtain the attribute value. + * @param string the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute + */ + public function getAttribute($name) + { + if(property_exists($this,$name)) + return $this->$name; + else if(isset($this->_attributes[$name])) + return $this->_attributes[$name]; + } + + /** + * Sets the named attribute value. + * You may also use $this->AttributeName to set the attribute value. + * @param string the attribute name + * @param mixed the attribute value. + * @return boolean whether the attribute exists and the assignment is conducted successfully + * @see hasAttribute + */ + public function setAttribute($name,$value) + { + if(property_exists($this,$name)) + $this->$name=$value; + else if(isset($this->getMetaData()->columns[$name])) + $this->_attributes[$name]=$value; + else + return false; + return true; + } + + /** + * Adds a related object to this record. + * This method is used internally by {@link CActiveFinder} to populate related objects. + * @param string attribute name + * @param mixed the related record + * @param boolean whether the relation is HAS_MANY/MANY_MANY. + */ + public function addRelatedRecord($name,$record,$multiple) + { + if($multiple) + { + if(!isset($this->_related[$name])) + $this->_related[$name]=array(); + if($record instanceof TActiveRecord) + $this->_related[$name][]=$record; + } + else if(!isset($this->_related[$name])) + $this->_related[$name]=$record; + } + + /** + * Returns all column attribute values. + * Note, related objects are not returned. + * @param mixed names of attributes whose value needs to be returned. + * If this is true (default), then all attribute values will be returned, including + * those that are not loaded from DB (null will be returned for those attributes). + * If this is null, all attributes except those that are not loaded from DB will be returned. + * @return array attribute values indexed by attribute names. + */ + public function getAttributes($names=true) + { + $attributes=$this->_attributes; + foreach($this->getMetaData()->columns as $name=>$column) + { + if(property_exists($this,$name)) + $attributes[$name]=$this->$name; + else if($names===true && !isset($attributes[$name])) + $attributes[$name]=null; + } + if(is_array($names)) + { + $attrs=array(); + foreach($names as $name) + $attrs[$name]=isset($attributes[$name])?$attributes[$name]:null; + return $attrs; + } + else + return $attributes; + } + + /** + * Saves the current record. + * + * The record is inserted as a row into the database table if its {@link isNewRecord} + * property is true (usually the case when the record is created using the 'new' + * operator). Otherwise, it will be used to update the corresponding row in the table + * (usually the case if the record is obtained using one of those 'find' methods.) + * + * Validation will be performed before saving the record. If the validation fails, + * the record will not be saved. You can call {@link getErrors()} to retrieve the + * validation errors. + * + * If the record is saved via insertion, its {@link isNewRecord} property will be + * set false, and its {@link scenario} property will be set to be 'update'. + * And if its primary key is auto-incremental and is not set before insertion, + * the primary key will be populated with the automatically generated key value. + * + * @param boolean whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation=true,$attributes=null) + { + if(!$runValidation || $this->validate($attributes)) + return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes); + else + return false; + } + + /** + * @return boolean whether the record is new and should be inserted when calling {@link save}. + * This property is automatically set in constructor and {@link populateRecord}. + * Defaults to false, but it will be set to true if the instance is created using + * the new operator. + */ + public function getIsNewRecord() + { + return $this->_new; + } + + /** + * @param boolean whether the record is new and should be inserted when calling {@link save}. + * @see getIsNewRecord + */ + public function setIsNewRecord($value) + { + $this->_new=$value; + } + + /** + * This event is raised before the record is saved. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onBeforeSave($event) + { + $this->raiseEvent('onBeforeSave',$event); + } + + /** + * This event is raised after the record is saved. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterSave($event) + { + $this->raiseEvent('onAfterSave',$event); + } + + /** + * This event is raised before the record is deleted. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onBeforeDelete($event) + { + $this->raiseEvent('onBeforeDelete',$event); + } + + /** + * This event is raised after the record is deleted. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterDelete($event) + { + $this->raiseEvent('onAfterDelete',$event); + } + + /** + * This event is raised after the record instance is created by new operator. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterConstruct($event) + { + $this->raiseEvent('onAfterConstruct',$event); + } + + /** + * This event is raised after the record is instantiated by a find method. + * @param CEvent the event parameter + * @since 1.0.2 + */ + 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. + * Use {@link isNewRecord} to determine whether the saving is + * for inserting or updating record. + * Make sure you call the parent implementation so that the event is raised properly. + * @return boolean whether the saving should be executed. Defaults to true. + */ + protected function beforeSave() + { + $this->onBeforeSave(new TEvent($this)); + return true; + } + + /** + * This method is invoked after saving a record. + * The default implementation raises the {@link onAfterSave} event. + * You may override this method to do postprocessing after record saving. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterSave() + { + $this->onAfterSave(new TEvent($this)); + } + + /** + * This method is invoked before deleting a record. + * The default implementation raises the {@link onBeforeDelete} event. + * You may override this method to do any preparation work for record deletion. + * Make sure you call the parent implementation so that the event is raised properly. + * @return boolean whether the record should be deleted. Defaults to true. + */ + protected function beforeDelete() + { + $this->onBeforeDelete(new TEvent($this)); + return true; + } + + /** + * This method is invoked after deleting a record. + * The default implementation raises the {@link onAfterDelete} event. + * You may override this method to do postprocessing after the record is deleted. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterDelete() + { + $this->onAfterDelete(new TEvent($this)); + } + + /** + * This method is invoked after a record instance is created by new operator. + * The default implementation raises the {@link onAfterConstruct} event. + * You may override this method to do postprocessing after record creation. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterConstruct() + { + $this->onAfterConstruct(new TEvent($this)); + } + + /** + * This method is invoked after each record is instantiated by a find method. + * The default implementation raises the {@link onAfterFind} event. + * You may override this method to do postprocessing after each newly found record is instantiated. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterFind() + { + $this->onAfterFind(new TEvent($this)); + } + + /** + * Calls {@link afterFind}. + * This method is internally used. + * @since 1.0.3 + */ + public function afterFindInternal() + { + $this->afterFind(); + } + + /** + * Inserts a row into the table based on this active record attributes. + * If the table's primary key is auto-incremental and is null before insertion, + * it will be populated with the actual value after insertion. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false, + * and its {@link scenario} property will be set to be 'update'. + * @param array list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws CException if the record is not new + */ + public function insert($attributes=null) + { + if(!$this->getIsNewRecord()) + throw new TDbException('The active record cannot be inserted to database because it is not new.'); + if($this->beforeSave()) + { + Prado::trace(get_class($this).'.insert()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getMetaData()->tableSchema; + $command=$builder->createInsertCommand($table,$this->getAttributes($attributes)); + if($command->execute()) + { + $primaryKey=$table->primaryKey; + if($table->sequenceName!==null) + { + if(is_string($primaryKey) && $this->$primaryKey===null) + $this->$primaryKey=$builder->getLastInsertID($table); + else if(is_array($primaryKey)) + { + foreach($primaryKey as $pk) + { + if($this->$pk===null) + { + $this->$pk=$builder->getLastInsertID($table); + break; + } + } + } + } + $this->afterSave(); + $this->setIsNewRecord(false); + $this->setScenario('update'); + return true; + } + else + $this->afterSave(); + } + else + return false; + } + + /** + * Updates the row represented by this active record. + * All loaded attributes will be saved to the database. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * @param array list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the update is successful + * @throws CException if the record is new + */ + public function update($attributes=null) + { + if($this->getIsNewRecord()) + throw new TDbException('The active record cannot be updated because it is new.'); + if($this->beforeSave()) + { + Prado::trace(get_class($this).'.update()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $this->updateByPk($this->getPrimaryKey(),$this->getAttributes($attributes)); + $this->afterSave(); + return true; + } + else + return false; + } + + /** + * Saves a selected list of attributes. + * Unlike {@link save}, this method only saves the specified attributes + * of an existing row dataset. It thus has better performance. + * Note, this method does neither attribute filtering nor validation. + * So do not use this method with untrusted data (such as user posted data). + * You may consider the following alternative if you want to do so: + *
+	 * $postRecord=Post::model()->findByPk($postID);
+	 * $postRecord->attributes=$_POST['post'];
+	 * $postRecord->save();
+	 * 
+ * @param array attributes to be updated. Each element represents an attribute name + * or an attribute value indexed by its name. If the latter, the record's + * attribute will be changed accordingly before saving. + * @return boolean whether the update is successful + * @throws CException if the record is new or any database error + */ + public function saveAttributes($attributes) + { + if(!$this->getIsNewRecord()) + { + Prado::trace(get_class($this).'.saveAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $values=array(); + foreach($attributes as $name=>$value) + { + if(is_integer($name)) + $values[$value]=$this->$value; + else + $values[$name]=$this->$name=$value; + } + return $this->updateByPk($this->getPrimaryKey(),$values)>0; + } + else + throw new TDbException('The active record cannot be updated because it is new.'); + } + + /** + * 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()) + { + Prado::trace(get_class($this).'.delete()','System.Testing.Data.ActiveRecord.TActiveRecord'); + if($this->beforeDelete()) + { + $result=$this->deleteByPk($this->getPrimaryKey())>0; + $this->afterDelete(); + return $result; + } + else + return false; + } + else + throw new TDbException('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() + { + Prado::trace(get_class($this).'.refresh()','System.Testing.Data.ActiveRecord.TActiveRecord'); + if(!$this->getIsNewRecord() && ($record=$this->findByPk($this->getPrimaryKey()))!==null) + { + $this->_attributes=array(); + $this->_related=array(); + foreach($this->getMetaData()->columns as $name=>$column) + $this->$name=$record->$name; + return true; + } + else + return false; + } + + /** + * Compares this active record with another one. + * The comparison is made by comparing the primary key values of the two active records. + * @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(); + } + + /** + * @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}; + else if(is_array($table->primaryKey)) + { + $values=array(); + foreach($table->primaryKey as $name) + $values[$name]=$this->$name; + return $values; + } + else + return null; + } + + private function query($criteria,$all=false) + { + $this->applyScopes($criteria); + $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria); + return $all ? $this->populateRecords($command->queryAll()) : $this->populateRecord($command->queryRow()); + } + + private function applyScopes(&$criteria) + { + if(($c=$this->getDbCriteria(false))!==null) + { + $c->mergeWith($criteria); + $criteria=$c; + $this->_c=null; + } + } + + /** + * Finds a single active record with the specified condition. + * @param mixed 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 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 CActiveRecord the record found. Null if no record is found. + */ + public function find($condition='',$params=array()) + { + Prado::trace(get_class($this).'.find()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); + $criteria->limit=1; + return $this->query($criteria); + } + + /** + * Finds all active records satisfying the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $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 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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return CActiveRecord the record found. Null if none is found. + */ + public function findByPk($pk,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.findByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + $criteria->limit=1; + 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 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 query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAllByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + 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 list of attribute values (indexed by attribute names) that the active records should match. + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return CActiveRecord the record found. Null if none is found. + */ + public function findByAttributes($attributes,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.findByAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); + $criteria->limit=1; + 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 list of attribute values (indexed by attribute names) that the active records should match. + * @param mixed query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAllByAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); + return $this->query($criteria,true); + } + + /** + * Finds a single active record with the specified SQL statement. + * @param string the SQL statement + * @param array parameters to be bound to the SQL statement + * @return CActiveRecord the record found. Null if none is found. + */ + public function findBySql($sql,$params=array()) + { + Prado::trace(get_class($this).'.findBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); + return $this->populateRecord($command->queryRow()); + } + + /** + * Finds all active records using the specified SQL statement. + * @param string the SQL statement + * @param array parameters to be bound to the SQL statement + * @return array the records found. An empty array is returned if none is found. + */ + public function findAllBySql($sql,$params=array()) + { + Prado::trace(get_class($this).'.findAllBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); + return $this->populateRecords($command->queryAll()); + } + + /** + * Finds the number of rows satisfying the specified query condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows satisfying the specified query condition. + */ + public function count($condition='',$params=array()) + { + Prado::trace(get_class($this).'.count()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); + } + + /** + * Finds the number of rows using the given SQL statement. + * See {@link find()} for detailed explanation about $condition and $params. + * @param string the SQL statement + * @param array parameters to be bound to the SQL statement + * @return integer the number of rows using the given SQL statement. + */ + public function countBySql($sql,$params=array()) + { + Prado::trace(get_class($this).'.countBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + return $this->getCommandBuilder()->createSqlCommand($sql,$params)->queryScalar(); + } + + /** + * Checks whether there is row satisfying the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return boolean whether there is row satisfying the specified condition. + */ + public function exists($condition,$params=array()) + { + Prado::trace(get_class($this).'.exists()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $table=$this->getTableSchema(); + $criteria->select=reset($table->columns)->rawName; + $criteria->limit=1; + $this->applyScopes($criteria); + return $builder->createFindCommand($table,$criteria)->queryRow()!==false; + } + + /** + * 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'=>'createTime'),
+	 * ))->findAll();
+	 * 
+ * + * This method returns a {@link CActiveFinder} instance that provides + * a set of find methods similar to that of CActiveRecord. + * + * 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 CActiveFinder the active finder instance. If no parameter is passed in, the object itself will be returned. + */ + public function with() + { + if(func_num_args()>0) + { + $with=func_get_args(); + if(is_array($with[0])) // the parameter is given as an array + $with=$with[0]; + $finder=new TActiveFinder($this,$with,$this->getDbCriteria(false)); + $this->_c=null; + return $finder; + } + else + 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 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 list of attributes (name=>$value) to be updated + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateByPk($pk,$attributes,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getTableSchema(); + $criteria=$builder->createPkCriteria($table,$pk,$condition,$params); + $this->applyScopes($criteria); + $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 list of attributes (name=>$value) to be updated + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateAll($attributes,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $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 the counters to be updated (column name=>increment value) + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateCounters($counters,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateCounters()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $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 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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteByPk($pk,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.deleteByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + $this->applyScopes($criteria); + $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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteAll($condition='',$params=array()) + { + Prado::trace(get_class($this).'.deleteAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $command=$builder->createDeleteCommand($this->getTableSchema(),$criteria); + return $command->execute(); + } + + /** + * Creates an active record with the given attributes. + * This method is internally used by the find methods. + * @param array attribute values (column name=>column value) + * @param boolean whether to call {@link afterFind} after the record is populated. + * This parameter is added in version 1.0.3. + * @return CActiveRecord 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->_md=$this->getMetaData(); + foreach($attributes as $name=>$value) + { + if(property_exists($record,$name)) + $record->$name=$value; + else if(isset($record->_md->columns[$name])) + $record->_attributes[$name]=$value; + } + $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 list of attribute values for the active records. + * @param boolean whether to call {@link afterFind} after each record is populated. + * This parameter is added in version 1.0.3. + * @return array list of active records. + */ + public function populateRecords($data,$callAfterFind=true) + { + $records=array(); + $md=$this->getMetaData(); + $table=$md->tableSchema; + foreach($data as $attributes) + { + $record=$this->instantiate($attributes); + $record->_md=$md; + foreach($attributes as $name=>$value) + { + if(property_exists($record,$name)) + $record->$name=$value; + else if(isset($record->_md->columns[$name])) + $record->_attributes[$name]=$value; + } + $record->attachBehaviors($record->behaviors()); + if($callAfterFind) + $record->afterFind(); + $records[]=$record; + } + return $records; + } + + /** + * Creates an active record instance. + * This method is called by {@link populateRecord} and {@link populateRecords}. + * You may override this method if the instance being created + * depends the attributes that are to be populated to the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array list of attribute values for the active records. + * @return CActiveRecord the active record + * @since 1.0.2 + */ + protected function instantiate($attributes) + { + $class=get_class($this); + $model=new $class(null); + $model->setScenario('update'); + return $model; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed the offset to check on + * @return boolean + * @since 1.0.2 + */ + public function offsetExists($offset) + { + return isset($this->getMetaData()->columns[$offset]); + } +} + + +/** + * CBaseActiveRelation is the base class for all active relations. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0.4 + */ +class TBaseActiveRelation extends TComponent +{ + /** + * @var string name of the related object + */ + public $name; + /** + * @var string name of the related active record class + */ + public $className; + /** + * @var string the foreign key in this relation + */ + public $foreignKey; + /** + * @var mixed list of column names (an array, or a string of names separated by commas) to be selected. + * Do not quote or prefix the column names unless they are used in an expression. + * In that case, you should prefix the column names with '??.'. + */ + public $select='*'; + /** + * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names + * referenced in the condition should be disambiguated with prefix '??.'. + */ + public $condition=''; + /** + * @var array the parameters that are to be bound to the condition. + * The keys are parameter placeholder names, and the values are parameter values. + */ + public $params=array(); + /** + * @var string GROUP BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix '??.'. + */ + public $group=''; + /** + * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix '??.'. + */ + public $having=''; + /** + * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix '??.'. + */ + public $order=''; + + /** + * Constructor. + * @param string name of the relation + * @param string name of the related active record class + * @param string foreign key for this relation + * @param array additional options (name=>value). The keys must be the property names of this class. + */ + public function __construct($name,$className,$foreignKey,$options=array()) + { + $this->name=$name; + $this->className=$className; + $this->foreignKey=$foreignKey; + foreach($options as $name=>$value) + $this->$name=$value; + } + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + if(isset($criteria['select']) && $this->select!==$criteria['select']) + { + if($this->select==='*') + $this->select=$criteria['select']; + else if($criteria['select']!=='*') + { + $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; + $select2=is_string($criteria['select'])?preg_split('/\s*,\s*/',trim($criteria['select']),-1,PREG_SPLIT_NO_EMPTY):$criteria['select']; + $this->select=array_merge($select1,array_diff($select2,$select1)); + } + } + + if(isset($criteria['condition']) && $this->condition!==$criteria['condition']) + { + if($this->condition==='') + $this->condition=$criteria['condition']; + else if($criteria['condition']!=='') + $this->condition="({$this->condition}) AND ({$criteria['condition']})"; + } + + 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==='') + $this->order=$criteria['order']; + else if($criteria['order']!=='') + $this->order.=', '.$criteria['order']; + } + + if(isset($criteria['group']) && $this->group!==$criteria['group']) + { + if($this->group==='') + $this->group=$criteria['group']; + else if($criteria['group']!=='') + $this->group.=', '.$criteria['group']; + } + + if(isset($criteria['having']) && $this->having!==$criteria['having']) + { + if($this->having==='') + $this->having=$criteria['having']; + else if($criteria['having']!=='') + $this->having="({$this->having}) AND ({$criteria['having']})"; + } + } +} + + +/** + * CStatRelation represents a statistical relational query. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0.4 + */ +class TStatRelation extends TBaseActiveRelation +{ + /** + * @var string the statistical expression. Defaults to 'COUNT(*)', meaning + * the count of child objects. + */ + public $select='COUNT(*)'; + /** + * @var mixed the default value to be assigned to those records that do not + * receive a statistical query result. Defaults to 0. + */ + public $defaultValue=0; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + parent::mergeWith($criteria); + + if(isset($criteria['defaultValue'])) + $this->defaultValue=$criteria['defaultValue']; + } +} + + +/** + * CActiveRelation is the base class for representing active relations that bring back related objects. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TActiveRelation extends TBaseActiveRelation +{ + /** + * @var string join type. Defaults to 'LEFT OUTER JOIN'. + */ + public $joinType='LEFT OUTER JOIN'; + /** + * @var string ON clause. The condition specified here will be appended to the joining condition using AND operator. + * @since 1.0.2 + */ + public $on=''; + /** + * @var string the alias for the table that this relation refers to. Defaults to null, meaning + * the alias will be the same as the relation name. + * @since 1.0.1 + */ + public $alias; + /** + * @var string|array specifies which related objects should be eagerly loaded when this related object is lazily loaded. + * For more details about this property, see {@link CActiveRecord::with()}. + */ + public $with=array(); + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + if(isset($criteria['condition']) && $this->on!==$criteria['condition']) + { + if($this->on==='') + $this->on=$criteria['condition']; + else if($criteria['condition']!=='') + $this->on="({$this->on}) AND ({$criteria['condition']})"; + } + unset($criteria['condition']); + + parent::mergeWith($criteria); + + if(isset($criteria['joinType'])) + $this->joinType=$criteria['joinType']; + + if(isset($criteria['on']) && $this->on!==$criteria['on']) + { + if($this->on==='') + $this->on=$criteria['on']; + else if($criteria['on']!=='') + $this->on="({$this->on}) AND ({$criteria['on']})"; + } + + if(isset($criteria['with'])) + $this->with=$criteria['with']; + + if(isset($criteria['alias'])) + $this->alias=$criteria['alias']; + + if(isset($criteria['together'])) + $this->together=$criteria['together']; + } +} + + +/** + * CBelongsToRelation represents the parameters specifying a BELONGS_TO relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TBelongsToRelation extends TActiveRelation +{ +} + + +/** + * CHasOneRelation represents the parameters specifying a HAS_ONE relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class THasOneRelation extends TActiveRelation +{ +} + + +/** + * CHasManyRelation represents the parameters specifying a HAS_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class THasManyRelation extends TActiveRelation +{ + /** + * @var integer limit of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no limit. + */ + public $limit=-1; + /** + * @var integer offset of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no offset. + */ + public $offset=-1; + /** + * @var boolean whether this table should be joined with the primary table. + * When setting this property to be false, the table associated with this relation will + * appear in a separate JOIN statement. Defaults to true, meaning the table will be joined + * together with the primary table. Note that in version 1.0.x, the default value of this property was false. + */ + public $together=true; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + parent::mergeWith($criteria); + if(isset($criteria['limit']) && $criteria['limit']>0) + $this->limit=$criteria['limit']; + + if(isset($criteria['offset']) && $criteria['offset']>=0) + $this->offset=$criteria['offset']; + } +} + + +/** + * CManyManyRelation represents the parameters specifying a MANY_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TManyManyRelation extends THasManyRelation +{ +} + + +/** + * CActiveRecordMetaData represents the meta-data for an Active Record class. + * + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TActiveRecordMetaData +{ + /** + * @var CDbTableSchema the table schema information + */ + public $tableSchema; + /** + * @var array table columns + */ + public $columns; + /** + * @var array list of relations + */ + public $relations=array(); + /** + * @var array attribute default values + */ + public $attributeDefaults=array(); + + private $_model; + + /** + * Constructor. + * @param CActiveRecord the model instance + */ + public function __construct($model) + { + $this->_model=$model; + + $tableName=$model->tableName(); + if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) + throw new TDbException('The table "'.$tableName.'" for active record class "'.get_class($model).'" cannot be found in the database.'); + + if($table->primaryKey===null) + $table->primaryKey=$model->primaryKey(); + $this->tableSchema=$table; + $this->columns=$table->columns; + + foreach($table->columns as $name=>$column) + { + if(!$column->isPrimaryKey && $column->defaultValue!==null) + $this->attributeDefaults[$name]=$column->defaultValue; + } + + foreach($model->relations() as $name=>$config) + { + if(isset($config[0],$config[1],$config[2])) // relation class, AR class, FK + $this->relations[$name]=new $config[0]($name,$config[1],$config[2],array_slice($config,3)); + else + throw new TDbException('Active record "'.get_class($model).'" has an invalid configuration for relation "'.$name.'". It must specify the relation type, the related active record class and the foreign key.'); + } + } +} + +/** + * TActiveRecordInvalidFinderResult class. + * TActiveRecordInvalidFinderResult defines the enumerable type for possible results + * if an invalid {@link TActiveRecord::__call magic-finder} invoked. + * + * The following enumerable values are defined: + * - Null: return null (default) + * - Exception: throws a TActiveRecordException + * + * @author Yves Berkholz + * @version $Id: TActiveRecord.php 2649 2009-05-11 07:44:55Z godzilla80@gmx.net $ + * @package System.Data.ActiveRecord + * @see TActiveRecordManager::setInvalidFinderResult + * @see TActiveRecordConfig::setInvalidFinderResult + * @see TActiveRecord::setInvalidFinderResult + * @since 3.1.5 + */ +class TActiveRecordInvalidFinderResult extends TEnumerable +{ + const Null = 'Null'; + const Exception = 'Exception'; +} -- cgit v1.2.3