diff --git a/framework/db/Exception.php b/framework/db/Exception.php new file mode 100644 index 0000000..242361f --- /dev/null +++ b/framework/db/Exception.php @@ -0,0 +1,39 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbException represents an exception that is caused by some DB-related operations. + * + * @author Qiang Xue + * @version $Id: CDbException.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db + * @since 1.0 + */ +class CDbException extends CException +{ + /** + * @var mixed the error info provided by a PDO exception. This is the same as returned + * by {@link http://www.php.net/manual/en/pdo.errorinfo.php PDO::errorInfo}. + * @since 1.1.4 + */ + public $errorInfo; + + /** + * Constructor. + * @param string $message PDO error message + * @param integer $code PDO error code + * @param mixed $errorInfo PDO error info + */ + public function __construct($message, $code = 0, $errorInfo = null) + { + $this->errorInfo = $errorInfo; + parent::__construct($message, $code); + } +} \ No newline at end of file diff --git a/framework/db/Migration.php b/framework/db/Migration.php new file mode 100644 index 0000000..84d1314 --- /dev/null +++ b/framework/db/Migration.php @@ -0,0 +1,387 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbMigration is the base class for representing a database migration. + * + * CDbMigration is designed to be used together with the "yiic migrate" command. + * + * Each child class of CDbMigration represents an individual database migration which + * is identified by the child class name. + * + * Within each migration, the {@link up} method contains the logic for "upgrading" + * the database used in an application; while the {@link down} method contains "downgrading" + * logic. The "yiic migrate" command manages all available migrations in an application. + * + * CDbMigration provides a set of convenient methods for manipulating database data and schema. + * For example, the {@link insert} method can be used to easily insert a row of data into + * a database table; the {@link createTable} method can be used to create a database table. + * Compared with the same methods in {@link CDbCommand}, these methods will display extra + * information showing the method parameters and execution time, which may be useful when + * applying migrations. + * + * @author Qiang Xue + * @version $Id: CDbMigration.php 3218 2011-05-13 00:06:44Z alexander.makarow $ + * @package system.db + * @since 1.1.6 + */ +abstract class CDbMigration extends CComponent +{ + private $_db; + + /** + * This method contains the logic to be executed when applying this migration. + * Child classes may implement this method to provide actual migration logic. + * @return boolean + */ + public function up() + { + $transaction = $this->getDbConnection()->beginTransaction(); + try + { + if ($this->safeUp() === false) + { + $transaction->rollBack(); + return false; + } + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; + echo $e->getTraceAsString() . "\n"; + $transaction->rollBack(); + return false; + } + } + + /** + * This method contains the logic to be executed when removing this migration. + * The default implementation throws an exception indicating the migration cannot be removed. + * Child classes may override this method if the corresponding migrations can be removed. + * @return boolean + */ + public function down() + { + $transaction = $this->getDbConnection()->beginTransaction(); + try + { + if ($this->safeDown() === false) + { + $transaction->rollBack(); + return false; + } + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; + echo $e->getTraceAsString() . "\n"; + $transaction->rollBack(); + return false; + } + } + + /** + * This method contains the logic to be executed when applying this migration. + * This method differs from {@link up} in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of {@link up} if the DB logic + * needs to be within a transaction. + * @return boolean + * @since 1.1.7 + */ + public function safeUp() + { + } + + /** + * This method contains the logic to be executed when removing this migration. + * This method differs from {@link down} in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of {@link up} if the DB logic + * needs to be within a transaction. + * @return boolean + * @since 1.1.7 + */ + public function safeDown() + { + } + + /** + * Returns the currently active database connection. + * By default, the 'db' application component will be returned and activated. + * You can call {@link setDbConnection} to switch to a different database connection. + * Methods such as {@link insert}, {@link createTable} will use this database connection + * to perform DB queries. + * @return CDbConnection the currently active database connection + */ + public function getDbConnection() + { + if ($this->_db === null) + { + $this->_db = Yii::app()->getComponent('db'); + if (!$this->_db instanceof CDbConnection) + throw new CException(Yii::t('yii', 'The "db" application component must be configured to be a CDbConnection object.')); + } + return $this->_db; + } + + /** + * Sets the currently active database connection. + * The database connection will be used by the methods such as {@link insert}, {@link createTable}. + * @param CDbConnection $db the database connection component + */ + public function setDbConnection($db) + { + $this->_db = $db; + } + + /** + * Executes a SQL statement. + * This method executes the specified SQL statement using {@link dbConnection}. + * @param string $sql the SQL statement to be executed + * @param array $params input parameters (name=>value) for the SQL execution. See {@link CDbCommand::execute} for more details. + * @since 1.1.7 + */ + public function execute($sql, $params = array()) + { + echo " > execute SQL: $sql ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand($sql)->execute($params); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes an INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name=>value) to be inserted into the table. + */ + public function insert($table, $columns) + { + echo " > insert into $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->insert($table, $columns); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes an UPDATE SQL statement. + * The method will properly escape the column names and bind the values to be updated. + * @param string $table the table to be updated. + * @param array $columns the column data (name=>value) to be updated. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link CDbCommand::where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function update($table, $columns, $conditions = '', $params = array()) + { + echo " > update $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->update($table, $columns, $conditions, $params); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes a DELETE SQL statement. + * @param string $table the table where the data will be deleted from. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link CDbCommand::where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function delete($table, $conditions = '', $params = array()) + { + echo " > delete from $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->delete($table, $conditions, $params); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The {@link getColumnType} method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + */ + public function createTable($table, $columns, $options = null) + { + echo " > create table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->createTable($table, $columns, $options); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + */ + public function renameTable($table, $newName) + { + echo " > rename table $table to $newName ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->renameTable($table, $newName); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + */ + public function dropTable($table) + { + echo " > drop table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->dropTable($table); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + */ + public function truncateTable($table) + { + echo " > truncate table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->truncateTable($table); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function addColumn($table, $column, $type) + { + echo " > add column $column $type to table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->addColumn($table, $column, $type); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + */ + public function dropColumn($table, $column) + { + echo " > drop column $column from table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->dropColumn($table, $column); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + */ + public function renameColumn($table, $name, $newName) + { + echo " > rename column $name in table $table to $newName ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->renameColumn($table, $name, $newName); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function alterColumn($table, $column, $type) + { + echo " > alter column $column in table $table to $type ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->alterColumn($table, $column, $type); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + echo " > add foreign key $name: $table ($columns) references $refTable ($refColumns) ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + */ + public function dropForeignKey($name, $table) + { + echo " > drop foreign key $name from table $table ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->dropForeignKey($name, $table); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + */ + public function createIndex($name, $table, $column, $unique = false) + { + echo " > create" . ($unique ? ' unique' : '') . " index $name on $table ($column) ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->createIndex($name, $table, $column, $unique); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + */ + public function dropIndex($name, $table) + { + echo " > drop index $name ..."; + $time = microtime(true); + $this->getDbConnection()->createCommand()->dropIndex($name, $table); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } +} \ No newline at end of file diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php new file mode 100644 index 0000000..cbd1618 --- /dev/null +++ b/framework/db/ar/ActiveRecord.php @@ -0,0 +1,2363 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 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 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + * + * @property array $attributes + */ +abstract class CActiveRecord extends CModel +{ + const BELONGS_TO = 'CBelongsToRelation'; + const HAS_ONE = 'CHasOneRelation'; + const HAS_MANY = 'CHasManyRelation'; + const MANY_MANY = 'CManyManyRelation'; + const STAT = 'CStatRelation'; + + /** + * @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. + */ + 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->init(); + + $this->attachBehaviors($this->behaviors()); + $this->afterConstruct(); + } + + /** + * Initializes this model. + * This method is invoked when an AR instance is newly created and has + * its {@link scenario} set. + * You may override this method to provide code that is needed to initialize the model (e.g. setting + * initial property values.) + * @since 1.0.8 + */ + public function init() + { + } + + /** + * 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 CActiveRecord the active record instance itself. + * @since 1.1.7 + */ + public function cache($duration, $dependency = null, $queryCount = 1) + { + $this->getDbConnection()->cache($duration, $dependency, $queryCount); + return $this; + } + + /** + * PHP sleep magic method. + * This method ensures that the model meta data reference is set to null. + * @return array + */ + 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 $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()->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); + } + + /** + * 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 ($this->setAttribute($name, $value) === false) + { + if (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 + * @since 1.0.1 + */ + public function __isset($name) + { + 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 + * @since 1.0.1 + */ + 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 + * @since 1.0.5 + */ + 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 $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}. + * @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 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.CActiveRecord'); + $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. + * @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 $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. + * @since 1.0.5 + */ + public function getDbCriteria($createIfNull = true) + { + if ($this->_c === null) + { + if (($c = $this->defaultScope()) !== array() || $createIfNull) + $this->_c = new CDbCriteria($c); + } + return $this->_c; + } + + /** + * Sets the query criteria for the current model. + * @param CDbCriteria $criteria the query criteria + * @since 1.1.3 + */ + public function setDbCriteria($criteria) + { + $this->_c = $criteria; + } + + /** + * Returns the default named scope that should be implicitly applied to all queries for this model. + * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. + * 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(); + } + + /** + * Resets all scopes and criterias applied including default scope. + * + * @return CActiveRecord + * @since 1.1.2 + */ + 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 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->_md = new CActiveRecordMetaData($model); + $model->attachBehaviors($model->behaviors()); + return $model; + } + } + + /** + * Returns the meta-data for this AR + * @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; + } + + /** + * 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. + * @since 1.0.8 + */ + public function refreshMetaData() + { + $finder = self::model(get_class($this)); + $finder->_md = new CActiveRecordMetaData($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() + { + 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: + *
    + *
  • BELONGS_TO: e.g. a member belongs to a team;
  • + *
  • HAS_ONE: e.g. a member has at most one profile;
  • + *
  • HAS_MANY: e.g. a team has many members;
  • + *
  • MANY_MANY: e.g. a member has many skills and a skill belongs to a member.
  • + *
+ * + * Besides the above relation types, a special relation called STAT is also supported + * that can be used to perform statistical query (or aggregational query). + * It retrieves the aggregational information about the related objects, such as the number + * of comments for each post, the average rating for each product, etc. + * + * Each kind of related objects is defined in this method as an array with the following elements: + *
+	 * 'varName'=>array('relationType', 'className', 'foreign_key', ...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 'foreign_key' states the foreign key that relates the two kinds of active record. + * Note, for composite foreign keys, they must be listed together, separated by commas; + * and for foreign keys used in MANY_MANY relation, the joining table must be declared as well + * (e.g. 'join_table(fk1, fk2)'). + * + * Additional options may be specified as name-value pairs in the rest array elements: + *
    + *
  • 'select': string|array, a list of columns to be selected. Defaults to '*', meaning all columns. + * Column names should be disambiguated if they appear in an expression (e.g. COUNT(relationName.name) AS name_count).
  • + *
  • 'condition': string, the WHERE clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age>20)
  • + *
  • 'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age DESC)
  • + *
  • 'with': string|array, a list of child related objects that should be loaded together with this object. + * Note, this is only honored by lazy loading, not eager loading.
  • + *
  • 'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.
  • + *
  • '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. + * 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 + * to the joining condition using the AND operator. This option has been + * available since version 1.0.2.
  • + *
  • 'index': the name of the column whose values should be used as keys + * of the array that stores related objects. This option is only available to + * HAS_MANY and MANY_MANY relations. This option has been available since version 1.0.7.
  • + *
  • 'scopes': scopes to apply. In case of a single scope can be used like 'scopes'=>'scopeName', + * in case of multiple scopes can be used like 'scopes'=>array('scopeName1','scopeName2'). + * This option has been available since version 1.1.9.
  • + *
+ * + * The following options are available for certain relations when lazy loading: + *
    + *
  • 'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • + *
  • 'having': string, the HAVING clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • + *
  • 'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • + *
  • 'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • + *
  • 'through': name of the model's relation that will be used as a bridge when getting related data. Can be set only for HAS_ONE and HAS_MANY. This option has been available since version 1.1.7.
  • + *
+ * + * 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'),
+	 * );
+	 * 
+ * + * @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'=>'create_time 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 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 + * @since 1.1.4 + */ + 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 = CActiveRecord::model($relations[$seg]->className); + else + break; + } + return $model->getAttributeLabel($name); + } + else + return $this->generateAttributeLabel($attribute); + } + + /** + * 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() + { + if (self::$db !== null) + return self::$db; + else + { + self::$db = Yii::app()->getDb(); + if (self::$db instanceof CDbConnection) + return self::$db; + else + throw new CDbException(Yii::t('yii', 'Active Record requires a "db" CDbConnection application component.')); + } + } + + /** + * Returns the named relation declared for this AR class. + * @param string $name 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; + } + + /** + * Returns the metadata of the table that this AR belongs to + * @return CDbTableSchema the metadata of the table that this AR belongs to + */ + public function getTableSchema() + { + return $this->getMetaData()->tableSchema; + } + + /** + * 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). + */ + 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 $name 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; + elseif (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 $name the attribute name + * @param mixed $value 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; + elseif (isset($this->getMetaData()->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 CActiveRecord) + { + 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. + * 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; + elseif ($names === true && !isset($attributes[$name])) + $attributes[$name] = null; + } + if (is_array($names)) + { + $attrs = array(); + foreach ($names as $name) + { + if (property_exists($this, $name)) + $attrs[$name] = $this->$name; + else + $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 $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the 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; + } + + /** + * Returns if the current record is new. + * @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; + } + + /** + * Sets if the record is new. + * @param boolean $value 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. + * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. + * @param CModelEvent $event 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 $event the event parameter + * @since 1.0.2 + */ + public function onAfterSave($event) + { + $this->raiseEvent('onAfterSave', $event); + } + + /** + * This event is raised before the record is deleted. + * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. + * @param CModelEvent $event 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 $event the event parameter + * @since 1.0.2 + */ + public function onAfterDelete($event) + { + $this->raiseEvent('onAfterDelete', $event); + } + + /** + * 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 + * @since 1.0.9 + */ + 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 + * @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() + { + if ($this->hasEventHandler('onBeforeSave')) + { + $event = new CModelEvent($this); + $this->onBeforeSave($event); + return $event->isValid; + } + else + return true; + } + + /** + * This method is invoked after saving a record successfully. + * 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() + { + if ($this->hasEventHandler('onAfterSave')) + $this->onAfterSave(new CEvent($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() + { + if ($this->hasEventHandler('onBeforeDelete')) + { + $event = new CModelEvent($this); + $this->onBeforeDelete($event); + return $event->isValid; + } + else + 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() + { + if ($this->hasEventHandler('onAfterDelete')) + $this->onAfterDelete(new CEvent($this)); + } + + /** + * This method is invoked before an AR finder executes a find call. + * The find calls include {@link find}, {@link findAll}, {@link findByPk}, + * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}. + * The default implementation raises the {@link onBeforeFind} event. + * If you override this method, make sure you call the parent implementation + * so that the event is raised properly. + * + * Starting from version 1.1.5, this method may be called with a hidden {@link CDbCriteria} + * parameter which represents the current query criteria as passed to a find method of AR. + * + * @since 1.0.9 + */ + protected function beforeFind() + { + if ($this->hasEventHandler('onBeforeFind')) + { + $event = new CModelEvent($this); + // for backward compatibility + $event->criteria = func_num_args() > 0 ? func_get_arg(0) : null; + $this->onBeforeFind($event); + } + } + + /** + * 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() + { + if ($this->hasEventHandler('onAfterFind')) + $this->onAfterFind(new CEvent($this)); + } + + /** + * Calls {@link beforeFind}. + * This method is internally used. + * @since 1.0.11 + */ + public function beforeFindInternal() + { + $this->beforeFind(); + } + + /** + * 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 $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws CException if the record is not new + */ + 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()) + { + Yii::trace(get_class($this) . '.insert()', 'system.db.ar.CActiveRecord'); + $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); + elseif (is_array($primaryKey)) + { + foreach ($primaryKey as $pk) + { + if ($this->$pk === null) + { + $this->$pk = $builder->getLastInsertID($table); + break; + } + } + } + } + $this->_pk = $this->getPrimaryKey(); + $this->afterSave(); + $this->setIsNewRecord(false); + $this->setScenario('update'); + return true; + } + } + 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 $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the update is successful + * @throws CException if the record is new + */ + 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()) + { + Yii::trace(get_class($this) . '.update()', 'system.db.ar.CActiveRecord'); + 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; + } + + /** + * Saves a selected list of attributes. + * Unlike {@link save}, this method only saves the specified attributes + * of an existing row dataset and does NOT call either {@link beforeSave} or {@link afterSave}. + * Also note that 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 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()) + { + Yii::trace(get_class($this) . '.saveAttributes()', 'system.db.ar.CActiveRecord'); + $values = array(); + foreach ($attributes as $name => $value) + { + 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 + * @since 1.1.8 + */ + public function saveCounters($counters) + { + Yii::trace(get_class($this) . '.saveCounters()', 'system.db.ar.CActiveRecord'); + $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.CActiveRecord'); + 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.CActiveRecord'); + 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 CActiveRecord $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). + * @since 1.1.0 + */ + 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. + * @since 1.1.0 + */ + public function getOldPrimaryKey() + { + return $this->_pk; + } + + /** + * Sets the old primary key value. + * @param mixed $value the old primary key value. + * @since 1.1.3 + */ + 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 + * @since 1.1.7 + */ + 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}. + * @since 1.0.12 + */ + 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; + } + + 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 + * @since 1.1.1 + */ + 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. + * @since 1.1.3 + */ + 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 CActiveRecord the record found. Null if no record is found. + */ + public function find($condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.find()', 'system.db.ar.CActiveRecord'); + $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.CActiveRecord'); + $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 CActiveRecord the record found. Null if none is found. + */ + public function findByPk($pk, $condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.findByPk()', 'system.db.ar.CActiveRecord'); + $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.CActiveRecord'); + $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 CActiveRecord the record found. Null if none is found. + */ + public function findByAttributes($attributes, $condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.findByAttributes()', 'system.db.ar.CActiveRecord'); + $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.CActiveRecord'); + $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 CActiveRecord the record found. Null if none is found. + */ + public function findBySql($sql, $params = array()) + { + Yii::trace(get_class($this) . '.findBySql()', 'system.db.ar.CActiveRecord'); + $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); + } + else + { + $command = $this->getCommandBuilder()->createSqlCommand($sql, $params); + return $this->populateRecord($command->queryRow()); + } + } + + /** + * 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. + */ + public function findAllBySql($sql, $params = array()) + { + Yii::trace(get_class($this) . '.findAllBySql()', 'system.db.ar.CActiveRecord'); + $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); + } + else + { + $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 $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. + */ + public function count($condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.count()', 'system.db.ar.CActiveRecord'); + $builder = $this->getCommandBuilder(); + $criteria = $builder->createCriteria($condition, $params); + $this->applyScopes($criteria); + + if (empty($criteria->with)) + return $builder->createCountCommand($this->getTableSchema(), $criteria)->queryScalar(); + else + { + $finder = new CActiveFinder($this, $criteria->with); + return $finder->count($criteria); + } + } + + /** + * 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. + * @since 1.1.4 + */ + public function countByAttributes($attributes, $condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.countByAttributes()', 'system.db.ar.CActiveRecord'); + $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 + { + $finder = new CActiveFinder($this, $criteria->with); + return $finder->count($criteria); + } + } + + /** + * 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. + */ + public function countBySql($sql, $params = array()) + { + Yii::trace(get_class($this) . '.countBySql()', 'system.db.ar.CActiveRecord'); + 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 $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. + */ + public function exists($condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.exists()', 'system.db.ar.CActiveRecord'); + $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 + { + $criteria->select = '*'; + $finder = new CActiveFinder($this, $criteria->with); + return $finder->count($criteria) > 0; + } + } + + /** + * 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 CActiveRecord the AR object itself. + */ + 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]; + if (!empty($with)) + $this->getDbCriteria()->mergeWith(array('with' => $with)); + } + 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 CActiveRecord the AR object itself + * @since 1.1.4 + */ + 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.CActiveRecord'); + $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.CActiveRecord'); + $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.CActiveRecord'); + $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.CActiveRecord'); + $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 + */ + public function deleteAll($condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.deleteAll()', 'system.db.ar.CActiveRecord'); + $builder = $this->getCommandBuilder(); + $criteria = $builder->createCriteria($condition, $params); + $command = $builder->createDeleteCommand($this->getTableSchema(), $criteria); + return $command->execute(); + } + + /** + * 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. + * @since 1.0.9 + */ + public function deleteAllByAttributes($attributes, $condition = '', $params = array()) + { + Yii::trace(get_class($this) . '.deleteAllByAttributes()', 'system.db.ar.CActiveRecord'); + $builder = $this->getCommandBuilder(); + $table = $this->getTableSchema(); + $criteria = $builder->createColumnCriteria($table, $attributes, $condition, $params); + $command = $builder->createDeleteCommand($table, $criteria); + return $command->execute(); + } + + /** + * Creates an active record with the given attributes. + * This method is internally used by the find methods. + * @param array $attributes attribute values (column name=>column value) + * @param boolean $callAfterFind 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->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) + { + $records = array(); + foreach ($data as $attributes) + { + if (($record = $this->populateRecord($attributes, $callAfterFind)) !== null) + { + if ($index === null) + $records[] = $record; + else + $records[$record->$index] = $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 $attributes 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); + return $model; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean + * @since 1.0.2 + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } +} + + +/** + * CBaseActiveRelation is the base class for all active relations. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0.4 + */ +class CBaseActiveRelation extends CComponent +{ + /** + * @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 'relationName.'. + */ + public $select = '*'; + /** + * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names + * referenced in the condition should be disambiguated with prefix 'relationName.'. + */ + 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 'relationName.'. + */ + public $group = ''; + /** + * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. + * For example, 'LEFT JOIN users ON users.id=authorID'. + * @since 1.1.3 + */ + public $join = ''; + /** + * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix 'relationName.'. + */ + public $having = ''; + /** + * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix 'relationName.'. + */ + public $order = ''; + + /** + * Constructor. + * @param string $name name of the relation + * @param string $className name of the related active record class + * @param string $foreignKey foreign key for this relation + * @param array $options 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 $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + * @since 1.0.5 + */ + public function mergeWith($criteria, $fromScope = false) + { + if ($criteria instanceof CDbCriteria) + $criteria = $criteria->toArray(); + 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; + $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']; + elseif ($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']; + elseif ($criteria['order'] !== '') + $this->order = $criteria['order'] . ', ' . $this->order; + } + + 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 === '') + $this->join = $criteria['join']; + elseif ($criteria['join'] !== '') + $this->join .= ' ' . $criteria['join']; + } + + 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']})"; + } + } +} + + +/** + * CStatRelation represents a statistical relational query. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0.4 + */ +class CStatRelation extends CBaseActiveRelation +{ + /** + * @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 $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + * @since 1.0.5 + */ + public function mergeWith($criteria, $fromScope = false) + { + if ($criteria instanceof CDbCriteria) + $criteria = $criteria->toArray(); + parent::mergeWith($criteria, $fromScope); + + 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 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CActiveRelation extends CBaseActiveRelation +{ + /** + * @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(); + /** + * @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. + * If this property is set true, then the corresponding table will ALWAYS be joined together + * with the primary table, no matter the primary table is limited or not. + * If this property is not set, the corresponding table will be joined with the primary table + * only when the primary table is not limited. + */ + public $together; + /** + * @var mixed scopes to apply + * Can be set to the one of the following: + *
    + *
  • Single scope: 'scopes'=>'scopeName'.
  • + *
  • Multiple scopes: 'scopes'=>array('scopeName1','scopeName2').
  • + *
+ * @since 1.1.9 + */ + public $scopes; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + * @since 1.0.5 + */ + public function mergeWith($criteria, $fromScope = false) + { + if ($criteria instanceof CDbCriteria) + $criteria = $criteria->toArray(); + 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'])) + $this->joinType = $criteria['joinType']; + + 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'])) + $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 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CBelongsToRelation extends CActiveRelation +{ +} + + +/** + * CHasOneRelation represents the parameters specifying a HAS_ONE relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CHasOneRelation extends CActiveRelation +{ + /** + * @var string the name of the relation that should be used as the bridge to this relation. + * Defaults to null, meaning don't use any bridge. + * @since 1.1.7 + */ + public $through; +} + + +/** + * CHasManyRelation represents the parameters specifying a HAS_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CHasManyRelation extends CActiveRelation +{ + /** + * @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 string the name of the column that should be used as the key for storing related objects. + * Defaults to null, meaning using zero-based integer IDs. + * @since 1.0.7 + */ + public $index; + /** + * @var string the name of the relation that should be used as the bridge to this relation. + * Defaults to null, meaning don't use any bridge. + * @since 1.1.7 + */ + public $through; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + * @since 1.0.5 + */ + public function mergeWith($criteria, $fromScope = false) + { + if ($criteria instanceof CDbCriteria) + $criteria = $criteria->toArray(); + parent::mergeWith($criteria, $fromScope); + if (isset($criteria['limit']) && $criteria['limit'] > 0) + $this->limit = $criteria['limit']; + + if (isset($criteria['offset']) && $criteria['offset'] >= 0) + $this->offset = $criteria['offset']; + + if (isset($criteria['index'])) + $this->index = $criteria['index']; + } +} + + +/** + * CManyManyRelation represents the parameters specifying a MANY_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CManyManyRelation extends CHasManyRelation +{ +} + + +/** + * CActiveRecordMetaData represents the meta-data for an Active Record class. + * + * @author Qiang Xue + * @version $Id: CActiveRecord.php 3344 2011-07-06 22:04:50Z alexander.makarow $ + * @package system.db.ar + * @since 1.0 + */ +class CActiveRecordMetaData +{ + /** + * @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 $model the model instance + */ + public function __construct($model) + { + $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; + } + } + } + $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) + { + $this->addRelation($name, $config); + } + } + + /** + * Adds a relation. + * + * $config is an array with three elements: + * relation type, the related active record class and the foreign key. + * + * @throws CDbException + * @param string $name $name Name of the relation. + * @param array $config $config Relation parameters. + * @return void + * @since 1.1.2 + */ + public function addRelation($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 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 + * @since 1.1.2 + */ + public function hasRelation($name) + { + return isset($this->relations[$name]); + } + + /** + * Deletes a relation with specified name. + * + * @param string $name $name + * @return void + * @since 1.1.2 + */ + public function removeRelation($name) + { + unset($this->relations[$name]); + } +} diff --git a/framework/db/ar/ActiveRecordBehavior.php b/framework/db/ar/ActiveRecordBehavior.php new file mode 100644 index 0000000..509e175 --- /dev/null +++ b/framework/db/ar/ActiveRecordBehavior.php @@ -0,0 +1,97 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CActiveRecordBehavior is the base class for behaviors that can be attached to {@link CActiveRecord}. + * Compared with {@link CModelBehavior}, CActiveRecordBehavior attaches to more events + * that are only defined by {@link CActiveRecord}. + * + * @author Qiang Xue + * @version $Id: CActiveRecordBehavior.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db.ar + * @since 1.0.2 + */ +class CActiveRecordBehavior extends CModelBehavior +{ + /** + * Declares events and the corresponding event handler methods. + * If you override this method, make sure you merge the parent result to the return value. + * @return array events (array keys) and the corresponding event handler methods (array values). + * @see CBehavior::events + */ + public function events() + { + return array_merge(parent::events(), array( + 'onBeforeSave' => 'beforeSave', + 'onAfterSave' => 'afterSave', + 'onBeforeDelete' => 'beforeDelete', + 'onAfterDelete' => 'afterDelete', + 'onBeforeFind' => 'beforeFind', + 'onAfterFind' => 'afterFind', + )); + } + + /** + * Responds to {@link CActiveRecord::onBeforeSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the saving process. + * @param CModelEvent $event event parameter + */ + public function beforeSave($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CModelEvent $event event parameter + */ + public function afterSave($event) + { + } + + /** + * Responds to {@link CActiveRecord::onBeforeDelete} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the deletion process. + * @param CEvent $event event parameter + */ + public function beforeDelete($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterDelete} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + */ + public function afterDelete($event) + { + } + + /** + * Responds to {@link CActiveRecord::onBeforeFind} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + * @since 1.0.9 + */ + public function beforeFind($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterFind} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + */ + public function afterFind($event) + { + } +} diff --git a/framework/db/pdo/Command.php b/framework/db/pdo/Command.php new file mode 100644 index 0000000..92ff6a9 --- /dev/null +++ b/framework/db/pdo/Command.php @@ -0,0 +1,1499 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbCommand represents an SQL statement to execute against a database. + * + * It is usually created by calling {@link CDbConnection::createCommand}. + * The SQL statement to be executed may be set via {@link setText Text}. + * + * To execute a non-query SQL (such as insert, delete, update), call + * {@link execute}. To execute an SQL statement that returns result data set + * (such as SELECT), use {@link query} or its convenient versions {@link queryRow}, + * {@link queryColumn}, or {@link queryScalar}. + * + * If an SQL statement returns results (such as a SELECT SQL), the results + * can be accessed via the returned {@link CDbDataReader}. + * + * CDbCommand supports SQL statment preparation and parameter binding. + * Call {@link bindParam} to bind a PHP variable to a parameter in SQL. + * Call {@link bindValue} to bind a value to an SQL parameter. + * When binding a parameter, the SQL statement is automatically prepared. + * You may also call {@link prepare} to explicitly prepare an SQL statement. + * + * Starting from version 1.1.6, CDbCommand can also be used as a query builder + * that builds a SQL statement from code fragments. For example, + *
+ * $user = Yii::app()->db->createCommand()
+ *     ->select('username, password')
+ *     ->from('tbl_user')
+ *     ->where('id=:id', array(':id'=>1))
+ *     ->queryRow();
+ * 
+ * + * @author Qiang Xue + * @version $Id: CDbCommand.php 3240 2011-05-25 19:22:47Z qiang.xue $ + * @package system.db + * @since 1.0 + */ +class CDbCommand extends CComponent +{ + /** + * @var array the parameters (name=>value) to be bound to the current query. + * @since 1.1.6 + */ + public $params = array(); + + private $_connection; + private $_text; + private $_statement; + private $_paramLog = array(); + private $_query; + private $_fetchMode = array(PDO::FETCH_ASSOC); + + /** + * Constructor. + * @param CDbConnection $connection the database connection + * @param mixed $query the DB query to be executed. This can be either + * a string representing a SQL statement, or an array whose name-value pairs + * will be used to set the corresponding properties of the created command object. + * + * For example, you can pass in either 'SELECT * FROM tbl_user' + * or array('select'=>'*', 'from'=>'tbl_user'). They are equivalent + * in terms of the final query result. + * + * When passing the query as an array, the following properties are commonly set: + * {@link select}, {@link distinct}, {@link from}, {@link where}, {@link join}, + * {@link group}, {@link having}, {@link order}, {@link limit}, {@link offset} and + * {@link union}. Please refer to the setter of each of these properties for details + * about valid property values. This feature has been available since version 1.1.6. + * + * Since 1.1.7 it is possible to use a specific mode of data fetching by setting + * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php} + * for more details. + */ + public function __construct(CDbConnection $connection, $query = null) + { + $this->_connection = $connection; + if (is_array($query)) + { + foreach ($query as $name => $value) + $this->$name = $value; + } + else + $this->setText($query); + } + + /** + * Set the statement to null when serializing. + * @return array + */ + public function __sleep() + { + $this->_statement = null; + return array_keys(get_object_vars($this)); + } + + /** + * Set the default fetch mode for this statement + * @param mixed $mode fetch mode + * @return CDbCommand + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + * @since 1.1.7 + */ + public function setFetchMode($mode) + { + $params = func_get_args(); + $this->_fetchMode = $params; + return $this; + } + + /** + * Cleans up the command and prepares for building a new query. + * This method is mainly used when a command object is being reused + * multiple times for building different queries. + * Calling this method will clean up all internal states of the command object. + * @return CDbCommand this command instance + * @since 1.1.6 + */ + public function reset() + { + $this->_text = null; + $this->_query = null; + $this->_statement = null; + $this->_paramLog = array(); + $this->params = array(); + return $this; + } + + /** + * @return string the SQL statement to be executed + */ + public function getText() + { + if ($this->_text == '' && !empty($this->_query)) + $this->setText($this->buildQuery($this->_query)); + return $this->_text; + } + + /** + * Specifies the SQL statement to be executed. + * Any previous execution will be terminated or cancel. + * @param string $value the SQL statement to be executed + * @return CDbCommand this command instance + */ + public function setText($value) + { + if ($this->_connection->tablePrefix !== null && $value != '') + $this->_text = preg_replace('/{{(.*?)}}/', $this->_connection->tablePrefix . '\1', $value); + else + $this->_text = $value; + $this->cancel(); + return $this; + } + + /** + * @return CDbConnection the connection associated with this command + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return PDOStatement the underlying PDOStatement for this command + * It could be null if the statement is not prepared yet. + */ + public function getPdoStatement() + { + return $this->_statement; + } + + /** + * Prepares the SQL statement to be executed. + * For complex SQL statement that is to be executed multiple times, + * this may improve performance. + * For SQL statement with binding parameters, this method is invoked + * automatically. + */ + public function prepare() + { + if ($this->_statement == null) + { + try + { + $this->_statement = $this->getConnection()->getPdoInstance()->prepare($this->getText()); + $this->_paramLog = array(); + } + catch(Exception $e) + { + Yii::log('Error in preparing SQL: ' . $this->getText(), CLogger::LEVEL_ERROR, 'system.db.CDbCommand'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + throw new CDbException(Yii::t('yii', 'CDbCommand failed to prepare the SQL statement: {error}', + array('{error}' => $e->getMessage())), (int)$e->getCode(), $errorInfo); + } + } + } + + /** + * Cancels the execution of the SQL statement. + */ + public function cancel() + { + $this->_statement = null; + } + + /** + * Binds a parameter to the SQL statement to be executed. + * @param mixed $name Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form :name. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value Name of the PHP variable to bind to the SQL statement parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @param integer $length length of the data type + * @param mixed $driverOptions the driver-specific options (this is available since version 1.1.6) + * @return CDbCommand the current command being executed (this is available since version 1.0.8) + * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php + */ + public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) + { + $this->prepare(); + if ($dataType === null) + $this->_statement->bindParam($name, $value, $this->_connection->getPdoType(gettype($value))); + elseif ($length === null) + $this->_statement->bindParam($name, $value, $dataType); + elseif ($driverOptions === null) + $this->_statement->bindParam($name, $value, $dataType, $length); + else + $this->_statement->bindParam($name, $value, $dataType, $length, $driverOptions); + $this->_paramLog[$name] =& $value; + return $this; + } + + /** + * Binds a value to a parameter. + * @param mixed $name Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form :name. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value The value to bind to the parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @return CDbCommand the current command being executed (this is available since version 1.0.8) + * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php + */ + public function bindValue($name, $value, $dataType = null) + { + $this->prepare(); + if ($dataType === null) + $this->_statement->bindValue($name, $value, $this->_connection->getPdoType(gettype($value))); + else + $this->_statement->bindValue($name, $value, $dataType); + $this->_paramLog[$name] = $value; + return $this; + } + + /** + * Binds a list of values to the corresponding parameters. + * This is similar to {@link bindValue} except that it binds multiple values. + * Note that the SQL data type of each value is determined by its PHP type. + * @param array $values the values to be bound. This must be given in terms of an associative + * array with array keys being the parameter names, and array values the corresponding parameter values. + * For example, array(':name'=>'John', ':age'=>25). + * @return CDbCommand the current command being executed + * @since 1.1.5 + */ + public function bindValues($values) + { + $this->prepare(); + foreach ($values as $name => $value) + { + $this->_statement->bindValue($name, $value, $this->_connection->getPdoType(gettype($value))); + $this->_paramLog[$name] = $value; + } + return $this; + } + + /** + * Executes the SQL statement. + * This method is meant only for executing non-query SQL statement. + * No result set will be returned. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return integer number of rows affected by the execution. + * @throws CException execution failed + */ + public function execute($params = array()) + { + if ($this->_connection->enableParamLogging && ($pars = array_merge($this->_paramLog, $params)) !== array()) + { + $p = array(); + foreach ($pars as $name => $value) + $p[$name] = $name . '=' . var_export($value, true); + $par = '. Bound with ' . implode(', ', $p); + } + else + $par = ''; + Yii::trace('Executing SQL: ' . $this->getText() . $par, 'system.db.CDbCommand'); + try + { + if ($this->_connection->enableProfiling) + Yii::beginProfile('system.db.CDbCommand.execute(' . $this->getText() . ')', 'system.db.CDbCommand.execute'); + + $this->prepare(); + if ($params === array()) + $this->_statement->execute(); + else + $this->_statement->execute($params); + $n = $this->_statement->rowCount(); + + if ($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.execute(' . $this->getText() . ')', 'system.db.CDbCommand.execute'); + + return $n; + } + catch(Exception $e) + { + if ($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.execute(' . $this->getText() . ')', 'system.db.CDbCommand.execute'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + $message = $e->getMessage(); + Yii::log(Yii::t('yii', 'CDbCommand::execute() failed: {error}. The SQL statement executed was: {sql}.', + array('{error}' => $message, '{sql}' => $this->getText() . $par)), CLogger::LEVEL_ERROR, 'system.db.CDbCommand'); + if (YII_DEBUG) + $message .= '. The SQL statement executed was: ' . $this->getText() . $par; + throw new CDbException(Yii::t('yii', 'CDbCommand failed to execute the SQL statement: {error}', + array('{error}' => $message)), (int)$e->getCode(), $errorInfo); + } + } + + /** + * Executes the SQL statement and returns query result. + * This method is for executing an SQL query that returns result set. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return CDbDataReader the reader object for fetching the query result + * @throws CException execution failed + */ + public function query($params = array()) + { + return $this->queryInternal('', 0, $params); + } + + /** + * Executes the SQL statement and returns all rows. + * @param boolean $fetchAssociative whether each row should be returned as an associated array with + * column names as the keys or the array keys are column indexes (0-based). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return array all rows of the query result. Each array element is an array representing a row. + * An empty array is returned if the query results in nothing. + * @throws CException execution failed + */ + public function queryAll($fetchAssociative = true, $params = array()) + { + return $this->queryInternal('fetchAll', $fetchAssociative ? $this->_fetchMode : PDO::FETCH_NUM, $params); + } + + /** + * Executes the SQL statement and returns the first row of the result. + * This is a convenient method of {@link query} when only the first row of data is needed. + * @param boolean $fetchAssociative whether the row should be returned as an associated array with + * column names as the keys or the array keys are column indexes (0-based). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return mixed the first row (in terms of an array) of the query result, false if no result. + * @throws CException execution failed + */ + public function queryRow($fetchAssociative = true, $params = array()) + { + return $this->queryInternal('fetch', $fetchAssociative ? $this->_fetchMode : PDO::FETCH_NUM, $params); + } + + /** + * Executes the SQL statement and returns the value of the first column in the first row of data. + * This is a convenient method of {@link query} when only a single scalar + * value is needed (e.g. obtaining the count of the records). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return mixed the value of the first column in the first row of the query result. False is returned if there is no value. + * @throws CException execution failed + */ + public function queryScalar($params = array()) + { + $result = $this->queryInternal('fetchColumn', 0, $params); + if (is_resource($result) && get_resource_type($result) === 'stream') + return stream_get_contents($result); + else + return $result; + } + + /** + * Executes the SQL statement and returns the first column of the result. + * This is a convenient method of {@link query} when only the first column of data is needed. + * Note, the column returned will contain the first element in each row of result. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return array the first column of the query result. Empty array if no result. + * @throws CException execution failed + */ + public function queryColumn($params = array()) + { + return $this->queryInternal('fetchAll', PDO::FETCH_COLUMN, $params); + } + + /** + * @param string $method method of PDOStatement to be called + * @param mixed $mode parameters to be passed to the method + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * This parameter has been available since version 1.0.10. + * @return mixed the method execution result + */ + private function queryInternal($method, $mode, $params = array()) + { + $params = array_merge($this->params, $params); + + if ($this->_connection->enableParamLogging && ($pars = array_merge($this->_paramLog, $params)) !== array()) + { + $p = array(); + foreach ($pars as $name => $value) + $p[$name] = $name . '=' . var_export($value, true); + $par = '. Bound with ' . implode(', ', $p); + } + else + $par = ''; + + Yii::trace('Querying SQL: ' . $this->getText() . $par, 'system.db.CDbCommand'); + + if ($this->_connection->queryCachingCount > 0 && $method !== '' + && $this->_connection->queryCachingDuration > 0 + && $this->_connection->queryCacheID !== false + && ($cache = Yii::app()->getComponent($this->_connection->queryCacheID)) !== null) + { + $this->_connection->queryCachingCount--; + $cacheKey = 'yii:dbquery' . $this->_connection->connectionString . ':' . $this->_connection->username; + $cacheKey .= ':' . $this->getText() . ':' . serialize(array_merge($this->_paramLog, $params)); + if (($result = $cache->get($cacheKey)) !== false) + { + Yii::trace('Query result found in cache', 'system.db.CDbCommand'); + return $result; + } + } + + try + { + if ($this->_connection->enableProfiling) + Yii::beginProfile('system.db.CDbCommand.query(' . $this->getText() . $par . ')', 'system.db.CDbCommand.query'); + + $this->prepare(); + if ($params === array()) + $this->_statement->execute(); + else + $this->_statement->execute($params); + + if ($method === '') + $result = new CDbDataReader($this); + else + { + $mode = (array)$mode; + $result = call_user_func_array(array($this->_statement, $method), $mode); + $this->_statement->closeCursor(); + } + + if ($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.query(' . $this->getText() . $par . ')', 'system.db.CDbCommand.query'); + + if (isset($cache, $cacheKey)) + $cache->set($cacheKey, $result, $this->_connection->queryCachingDuration, $this->_connection->queryCachingDependency); + + return $result; + } + catch(Exception $e) + { + if ($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.query(' . $this->getText() . $par . ')', 'system.db.CDbCommand.query'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + $message = $e->getMessage(); + Yii::log(Yii::t('yii', 'CDbCommand::{method}() failed: {error}. The SQL statement executed was: {sql}.', + array('{method}' => $method, '{error}' => $message, '{sql}' => $this->getText() . $par)), CLogger::LEVEL_ERROR, 'system.db.CDbCommand'); + if (YII_DEBUG) + $message .= '. The SQL statement executed was: ' . $this->getText() . $par; + throw new CDbException(Yii::t('yii', 'CDbCommand failed to execute the SQL statement: {error}', + array('{error}' => $message)), (int)$e->getCode(), $errorInfo); + } + } + + /** + * Builds a SQL SELECT statement from the given query specification. + * @param array $query the query specification in name-value pairs. The following + * query options are supported: {@link select}, {@link distinct}, {@link from}, + * {@link where}, {@link join}, {@link group}, {@link having}, {@link order}, + * {@link limit}, {@link offset} and {@link union}. + * @return string the SQL statement + * @since 1.1.6 + */ + public function buildQuery($query) + { + $sql = isset($query['distinct']) && $query['distinct'] ? 'SELECT DISTINCT' : 'SELECT'; + $sql .= ' ' . (isset($query['select']) ? $query['select'] : '*'); + + if (isset($query['from'])) + $sql .= "\nFROM " . $query['from']; + else + throw new CDbException(Yii::t('yii', 'The DB query must contain the "from" portion.')); + + if (isset($query['join'])) + $sql .= "\n" . (is_array($query['join']) ? implode("\n", $query['join']) : $query['join']); + + if (isset($query['where'])) + $sql .= "\nWHERE " . $query['where']; + + if (isset($query['group'])) + $sql .= "\nGROUP BY " . $query['group']; + + if (isset($query['having'])) + $sql .= "\nHAVING " . $query['having']; + + if (isset($query['order'])) + $sql .= "\nORDER BY " . $query['order']; + + $limit = isset($query['limit']) ? (int)$query['limit'] : -1; + $offset = isset($query['offset']) ? (int)$query['offset'] : -1; + if ($limit >= 0 || $offset > 0) + $sql = $this->_connection->getCommandBuilder()->applyLimit($sql, $limit, $offset); + + if (isset($query['union'])) + $sql .= "\nUNION (\n" . (is_array($query['union']) ? implode("\n) UNION (\n", $query['union']) : $query['union']) . ')'; + + return $sql; + } + + /** + * Sets the SELECT part of the query. + * @param mixed $columns the columns to be selected. Defaults to '*', meaning all columns. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. This parameter is supported since version 1.1.8. + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function select($columns = '*', $option = '') + { + if (is_string($columns) && strpos($columns, '(') !== false) + $this->_query['select'] = $columns; + else + { + if (!is_array($columns)) + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + + foreach ($columns as $i => $column) + { + if (is_object($column)) + $columns[$i] = (string)$column; + elseif (strpos($column, '(') === false) + { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $column, $matches)) + $columns[$i] = $this->_connection->quoteColumnName($matches[1]) . ' AS ' . $this->_connection->quoteColumnName($matches[2]); + else + $columns[$i] = $this->_connection->quoteColumnName($column); + } + } + $this->_query['select'] = implode(', ', $columns); + } + if ($option != '') + $this->_query['select'] = $option . ' ' . $this->_query['select']; + return $this; + } + + /** + * Returns the SELECT part in the query. + * @return string the SELECT part (without 'SELECT') in the query. + * @since 1.1.6 + */ + public function getSelect() + { + return isset($this->_query['select']) ? $this->_query['select'] : ''; + } + + /** + * Sets the SELECT part in the query. + * @param mixed $value the data to be selected. Please refer to {@link select()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setSelect($value) + { + $this->select($value); + } + + /** + * Sets the SELECT part of the query with the DISTINCT flag turned on. + * This is the same as {@link select} except that the DISTINCT flag is turned on. + * @param mixed $columns the columns to be selected. See {@link select} for more details. + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function selectDistinct($columns = '*') + { + $this->_query['distinct'] = true; + return $this->select($columns); + } + + /** + * Returns a value indicating whether SELECT DISTINCT should be used. + * @return boolean a value indicating whether SELECT DISTINCT should be used. + * @since 1.1.6 + */ + public function getDistinct() + { + return isset($this->_query['distinct']) ? $this->_query['distinct'] : false; + } + + /** + * Sets a value indicating whether SELECT DISTINCT should be used. + * @param boolean $value a value indicating whether SELECT DISTINCT should be used. + * @since 1.1.6 + */ + public function setDistinct($value) + { + $this->_query['distinct'] = $value; + } + + /** + * Sets the FROM part of the query. + * @param mixed $tables the table(s) to be selected from. This can be either a string (e.g. 'tbl_user') + * or an array (e.g. array('tbl_user', 'tbl_profile')) specifying one or several table names. + * Table names can contain schema prefixes (e.g. 'public.tbl_user') and/or table aliases (e.g. 'tbl_user u'). + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function from($tables) + { + if (is_string($tables) && strpos($tables, '(') !== false) + $this->_query['from'] = $tables; + else + { + if (!is_array($tables)) + $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); + foreach ($tables as $i => $table) + { + if (strpos($table, '(') === false) + { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) // with alias + $tables[$i] = $this->_connection->quoteTableName($matches[1]) . ' ' . $this->_connection->quoteTableName($matches[2]); + else + $tables[$i] = $this->_connection->quoteTableName($table); + } + } + $this->_query['from'] = implode(', ', $tables); + } + return $this; + } + + /** + * Returns the FROM part in the query. + * @return string the FROM part (without 'FROM' ) in the query. + * @since 1.1.6 + */ + public function getFrom() + { + return isset($this->_query['from']) ? $this->_query['from'] : ''; + } + + /** + * Sets the FROM part in the query. + * @param mixed $value the tables to be selected from. Please refer to {@link from()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setFrom($value) + { + $this->from($value); + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $conditions parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $conditions parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be of the format array(operator, operand1, operand2, ...), + * where the operator can be one of the followings, and the possible operands depend on the corresponding + * operator: + *
    + *
  • and: the operands should be concatenated together using AND. For example, + * array('and', 'id=1', 'id=2') will generate 'id=1 AND id=2'. If an operand is an array, + * it will be converted into a string using the same rules described here. For example, + * array('and', 'type=1', array('or', 'id=1', 'id=2')) will generate 'type=1 AND (id=1 OR id=2)'. + * The method will NOT do any quoting or escaping.
  • + *
  • or: similar as the and operator except that the operands are concatenated using OR.
  • + *
  • in: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * array('in', 'id', array(1,2,3)) will generate 'id IN (1,2,3)'. + * The method will properly quote the column name and escape values in the range.
  • + *
  • not in: similar as the in operator except that IN is replaced with NOT IN in the generated condition.
  • + *
  • like: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, array('like', 'name', '%tester%') will generate "name LIKE '%tester%'". + * When the value range is given as an array, multiple LIKE predicates will be generated and concatenated using AND. + * For example, array('like', 'name', array('%test%', '%sample%')) will generate + * "name LIKE '%test%' AND name LIKE '%sample%'". + * The method will properly quote the column name and escape values in the range.
  • + *
  • not like: similar as the like operator except that LIKE is replaced with NOT LIKE in the generated condition.
  • + *
  • or like: similar as the like operator except that OR is used to concatenated the LIKE predicates.
  • + *
  • or not like: similar as the not like operator except that OR is used to concatenated the NOT LIKE predicates.
  • + *
+ * @param mixed $conditions the conditions that should be put in the WHERE part. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function where($conditions, $params = array()) + { + $this->_query['where'] = $this->processConditions($conditions); + foreach ($params as $name => $value) + $this->params[$name] = $value; + return $this; + } + + /** + * Returns the WHERE part in the query. + * @return string the WHERE part (without 'WHERE' ) in the query. + * @since 1.1.6 + */ + public function getWhere() + { + return isset($this->_query['where']) ? $this->_query['where'] : ''; + } + + /** + * Sets the WHERE part in the query. + * @param mixed $value the where part. Please refer to {@link where()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setWhere($value) + { + $this->where($value); + } + + /** + * Appends an INNER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function join($table, $conditions, $params = array()) + { + return $this->joinInternal('join', $table, $conditions, $params); + } + + /** + * Returns the join part in the query. + * @return mixed the join part in the query. This can be an array representing + * multiple join fragments, or a string representing a single jojin fragment. + * Each join fragment will contain the proper join operator (e.g. LEFT JOIN). + * @since 1.1.6 + */ + public function getJoin() + { + return isset($this->_query['join']) ? $this->_query['join'] : ''; + } + + /** + * Sets the join part in the query. + * @param mixed $value the join part in the query. This can be either a string or + * an array representing multiple join parts in the query. Each part must contain + * the proper join operator (e.g. 'LEFT JOIN tbl_profile ON tbl_user.id=tbl_profile.id') + * @since 1.1.6 + */ + public function setJoin($value) + { + $this->_query['join'] = $value; + } + + /** + * Appends a LEFT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function leftJoin($table, $conditions, $params = array()) + { + return $this->joinInternal('left join', $table, $conditions, $params); + } + + /** + * Appends a RIGHT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function rightJoin($table, $conditions, $params = array()) + { + return $this->joinInternal('right join', $table, $conditions, $params); + } + + /** + * Appends a CROSS JOIN part to the query. + * Note that not all DBMS support CROSS JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function crossJoin($table) + { + return $this->joinInternal('cross join', $table); + } + + /** + * Appends a NATURAL JOIN part to the query. + * Note that not all DBMS support NATURAL JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function naturalJoin($table) + { + return $this->joinInternal('natural join', $table); + } + + /** + * Sets the GROUP BY part of the query. + * @param mixed $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function group($columns) + { + if (is_string($columns) && strpos($columns, '(') !== false) + $this->_query['group'] = $columns; + else + { + if (!is_array($columns)) + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + foreach ($columns as $i => $column) + { + if (is_object($column)) + $columns[$i] = (string)$column; + elseif (strpos($column, '(') === false) + $columns[$i] = $this->_connection->quoteColumnName($column); + } + $this->_query['group'] = implode(', ', $columns); + } + return $this; + } + + /** + * Returns the GROUP BY part in the query. + * @return string the GROUP BY part (without 'GROUP BY' ) in the query. + * @since 1.1.6 + */ + public function getGroup() + { + return isset($this->_query['group']) ? $this->_query['group'] : ''; + } + + /** + * Sets the GROUP BY part in the query. + * @param mixed $value the GROUP BY part. Please refer to {@link group()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setGroup($value) + { + $this->group($value); + } + + /** + * Sets the HAVING part of the query. + * @param mixed $conditions the conditions to be put after HAVING. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function having($conditions, $params = array()) + { + $this->_query['having'] = $this->processConditions($conditions); + foreach ($params as $name => $value) + $this->params[$name] = $value; + return $this; + } + + /** + * Returns the HAVING part in the query. + * @return string the HAVING part (without 'HAVING' ) in the query. + * @since 1.1.6 + */ + public function getHaving() + { + return isset($this->_query['having']) ? $this->_query['having'] : ''; + } + + /** + * Sets the HAVING part in the query. + * @param mixed $value the HAVING part. Please refer to {@link having()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setHaving($value) + { + $this->having($value); + } + + /** + * Sets the ORDER BY part of the query. + * @param mixed $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function order($columns) + { + if (is_string($columns) && strpos($columns, '(') !== false) + $this->_query['order'] = $columns; + else + { + if (!is_array($columns)) + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + foreach ($columns as $i => $column) + { + if (is_object($column)) + $columns[$i] = (string)$column; + elseif (strpos($column, '(') === false) + { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) + $columns[$i] = $this->_connection->quoteColumnName($matches[1]) . ' ' . strtoupper($matches[2]); + else + $columns[$i] = $this->_connection->quoteColumnName($column); + } + } + $this->_query['order'] = implode(', ', $columns); + } + return $this; + } + + /** + * Returns the ORDER BY part in the query. + * @return string the ORDER BY part (without 'ORDER BY' ) in the query. + * @since 1.1.6 + */ + public function getOrder() + { + return isset($this->_query['order']) ? $this->_query['order'] : ''; + } + + /** + * Sets the ORDER BY part in the query. + * @param mixed $value the ORDER BY part. Please refer to {@link order()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setOrder($value) + { + $this->order($value); + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit + * @param integer $offset the offset + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function limit($limit, $offset = null) + { + $this->_query['limit'] = (int)$limit; + if ($offset !== null) + $this->offset($offset); + return $this; + } + + /** + * Returns the LIMIT part in the query. + * @return string the LIMIT part (without 'LIMIT' ) in the query. + * @since 1.1.6 + */ + public function getLimit() + { + return isset($this->_query['limit']) ? $this->_query['limit'] : -1; + } + + /** + * Sets the LIMIT part in the query. + * @param integer $value the LIMIT part. Please refer to {@link limit()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setLimit($value) + { + $this->limit($value); + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function offset($offset) + { + $this->_query['offset'] = (int)$offset; + return $this; + } + + /** + * Returns the OFFSET part in the query. + * @return string the OFFSET part (without 'OFFSET' ) in the query. + * @since 1.1.6 + */ + public function getOffset() + { + return isset($this->_query['offset']) ? $this->_query['offset'] : -1; + } + + /** + * Sets the OFFSET part in the query. + * @param integer $value the OFFSET part. Please refer to {@link offset()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setOffset($value) + { + $this->offset($value); + } + + /** + * Appends a SQL statement using UNION operator. + * @param string $sql the SQL statement to be appended using UNION + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function union($sql) + { + if (isset($this->_query['union']) && is_string($this->_query['union'])) + $this->_query['union'] = array($this->_query['union']); + + $this->_query['union'][] = $sql; + + return $this; + } + + /** + * Returns the UNION part in the query. + * @return mixed the UNION part (without 'UNION' ) in the query. + * This can be either a string or an array representing multiple union parts. + * @since 1.1.6 + */ + public function getUnion() + { + return isset($this->_query['union']) ? $this->_query['union'] : ''; + } + + /** + * Sets the UNION part in the query. + * @param mixed $value the UNION part. This can be either a string or an array + * representing multiple SQL statements to be unioned together. + * @since 1.1.6 + */ + public function setUnion($value) + { + $this->_query['union'] = $value; + } + + /** + * Creates and executes an INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name=>value) to be inserted into the table. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function insert($table, $columns) + { + $params = array(); + $names = array(); + $placeholders = array(); + foreach ($columns as $name => $value) + { + $names[] = $this->_connection->quoteColumnName($name); + if ($value instanceof CDbExpression) + { + $placeholders[] = $value->expression; + foreach ($value->params as $n => $v) + $params[$n] = $v; + } + else + { + $placeholders[] = ':' . $name; + $params[':' . $name] = $value; + } + } + $sql = 'INSERT INTO ' . $this->_connection->quoteTableName($table) + . ' (' . implode(', ', $names) . ') VALUES (' + . implode(', ', $placeholders) . ')'; + return $this->setText($sql)->execute($params); + } + + /** + * Creates and executes an UPDATE SQL statement. + * The method will properly escape the column names and bind the values to be updated. + * @param string $table the table to be updated. + * @param array $columns the column data (name=>value) to be updated. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function update($table, $columns, $conditions = '', $params = array()) + { + $lines = array(); + foreach ($columns as $name => $value) + { + if ($value instanceof CDbExpression) + { + $lines[] = $this->_connection->quoteColumnName($name) . '=' . $value->expression; + foreach ($value->params as $n => $v) + $params[$n] = $v; + } + else + { + $lines[] = $this->_connection->quoteColumnName($name) . '=:' . $name; + $params[':' . $name] = $value; + } + } + $sql = 'UPDATE ' . $this->_connection->quoteTableName($table) . ' SET ' . implode(', ', $lines); + if (($where = $this->processConditions($conditions)) != '') + $sql .= ' WHERE ' . $where; + return $this->setText($sql)->execute($params); + } + + /** + * Creates and executes a DELETE SQL statement. + * @param string $table the table where the data will be deleted from. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function delete($table, $conditions = '', $params = array()) + { + $sql = 'DELETE FROM ' . $this->_connection->quoteTableName($table); + if (($where = $this->processConditions($conditions)) != '') + $sql .= ' WHERE ' . $where; + return $this->setText($sql)->execute($params); + } + + /** + * Builds and executes a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The {@link getColumnType} method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function createTable($table, $columns, $options = null) + { + return $this->setText($this->getConnection()->getSchema()->createTable($table, $columns, $options))->execute(); + } + + /** + * Builds and executes a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return $this->setText($this->getConnection()->getSchema()->renameTable($table, $newName))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropTable($table) + { + return $this->setText($this->getConnection()->getSchema()->dropTable($table))->execute(); + } + + /** + * Builds and executes a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function truncateTable($table) + { + $schema = $this->getConnection()->getSchema(); + $n = $this->setText($schema->truncateTable($table))->execute(); + if (strncasecmp($this->getConnection()->getDriverName(), 'sqlite', 6) === 0) + $schema->resetSequence($schema->getTable($table)); + return $n; + } + + /** + * Builds and executes a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function addColumn($table, $column, $type) + { + return $this->setText($this->getConnection()->getSchema()->addColumn($table, $column, $type))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropColumn($table, $column) + { + return $this->setText($this->getConnection()->getSchema()->dropColumn($table, $column))->execute(); + } + + /** + * Builds and executes a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + return $this->setText($this->getConnection()->getSchema()->renameColumn($table, $name, $newName))->execute(); + } + + /** + * Builds and executes a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + return $this->setText($this->getConnection()->getSchema()->alterColumn($table, $column, $type))->execute(); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + return $this->setText($this->getConnection()->getSchema()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update))->execute(); + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropForeignKey($name, $table) + { + return $this->setText($this->getConnection()->getSchema()->dropForeignKey($name, $table))->execute(); + } + + /** + * Builds and executes a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function createIndex($name, $table, $column, $unique = false) + { + return $this->setText($this->getConnection()->getSchema()->createIndex($name, $table, $column, $unique))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return $this->setText($this->getConnection()->getSchema()->dropIndex($name, $table))->execute(); + } + + /** + * Generates the condition string that will be put in the WHERE part + * @param mixed $conditions the conditions that will be put in the WHERE part. + * @return string the condition string to put in the WHERE part + */ + private function processConditions($conditions) + { + if (!is_array($conditions)) + return $conditions; + elseif ($conditions === array()) + return ''; + $n = count($conditions); + $operator = strtoupper($conditions[0]); + if ($operator === 'OR' || $operator === 'AND') + { + $parts = array(); + for ($i = 1;$i < $n;++$i) + { + $condition = $this->processConditions($conditions[$i]); + if ($condition !== '') + $parts[] = '(' . $condition . ')'; + } + return $parts === array() ? '' : implode(' ' . $operator . ' ', $parts); + } + + if (!isset($conditions[1], $conditions[2])) + return ''; + + $column = $conditions[1]; + if (strpos($column, '(') === false) + $column = $this->_connection->quoteColumnName($column); + + $values = $conditions[2]; + if (!is_array($values)) + $values = array($values); + + if ($operator === 'IN' || $operator === 'NOT IN') + { + if ($values === array()) + return $operator === 'IN' ? '0=1' : ''; + foreach ($values as $i => $value) + { + if (is_string($value)) + $values[$i] = $this->_connection->quoteValue($value); + else + $values[$i] = (string)$value; + } + return $column . ' ' . $operator . ' (' . implode(', ', $values) . ')'; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE' || $operator === 'OR LIKE' || $operator === 'OR NOT LIKE') + { + if ($values === array()) + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') + $andor = ' AND '; + else + { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + $expressions = array(); + foreach ($values as $value) + $expressions[] = $column . ' ' . $operator . ' ' . $this->_connection->quoteValue($value); + return implode($andor, $expressions); + } + + throw new CDbException(Yii::t('yii', 'Unknown operator "{operator}".', array('{operator}' => $operator))); + } + + /** + * Appends an JOIN part to the query. + * @param string $type the join type ('join', 'left join', 'right join', 'cross join', 'natural join') + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + private function joinInternal($type, $table, $conditions = '', $params = array()) + { + if (strpos($table, '(') === false) + { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) // with alias + $table = $this->_connection->quoteTableName($matches[1]) . ' ' . $this->_connection->quoteTableName($matches[2]); + else + $table = $this->_connection->quoteTableName($table); + } + + $conditions = $this->processConditions($conditions); + if ($conditions != '') + $conditions = ' ON ' . $conditions; + + if (isset($this->_query['join']) && is_string($this->_query['join'])) + $this->_query['join'] = array($this->_query['join']); + + $this->_query['join'][] = strtoupper($type) . ' ' . $table . $conditions; + + foreach ($params as $name => $value) + $this->params[$name] = $value; + return $this; + } +} diff --git a/framework/db/pdo/Connection.php b/framework/db/pdo/Connection.php new file mode 100644 index 0000000..4c99c57 --- /dev/null +++ b/framework/db/pdo/Connection.php @@ -0,0 +1,792 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * Connection represents a connection to a database. + * + * Connection works together with {@link CDbCommand}, {@link CDbDataReader} + * and {@link CDbTransaction} to provide data access to various DBMS + * in a common set of APIs. They are a thin wrapper of the {@link http://www.php.net/manual/en/ref.pdo.php PDO} + * PHP extension. + * + * To establish a connection, set {@link setActive active} to true after + * specifying {@link connectionString}, {@link username} and {@link password}. + * + * The following example shows how to create a Connection instance and establish + * the actual connection: + *
+ * $connection=new Connection($dsn,$username,$password);
+ * $connection->active=true;
+ * 
+ * + * After the DB connection is established, one can execute an SQL statement like the following: + *
+ * $command=$connection->createCommand($sqlStatement);
+ * $command->execute();   // a non-query SQL statement execution
+ * // or execute an SQL query and fetch the result set
+ * $reader=$command->query();
+ *
+ * // each $row is an array representing a row of data
+ * foreach($reader as $row) ...
+ * 
+ * + * One can do prepared SQL execution and bind parameters to the prepared SQL: + *
+ * $command=$connection->createCommand($sqlStatement);
+ * $command->bindParam($name1,$value1);
+ * $command->bindParam($name2,$value2);
+ * $command->execute();
+ * 
+ * + * To use transaction, do like the following: + *
+ * $transaction=$connection->beginTransaction();
+ * try
+ * {
+ *    $connection->createCommand($sql1)->execute();
+ *    $connection->createCommand($sql2)->execute();
+ *    //.... other SQL executions
+ *    $transaction->commit();
+ * }
+ * catch(Exception $e)
+ * {
+ *    $transaction->rollBack();
+ * }
+ * 
+ * + * Connection also provides a set of methods to support setting and querying + * of certain DBMS attributes, such as {@link getNullConversion nullConversion}. + * + * Since Connection implements the interface IApplicationComponent, it can + * be used as an application component and be configured in application configuration, + * like the following, + *
+ * array(
+ *     'components'=>array(
+ *         'db'=>array(
+ *             'class'=>'Connection',
+ *             'connectionString'=>'sqlite:path/to/dbfile',
+ *         ),
+ *     ),
+ * )
+ * 
+ * + * @author Qiang Xue + * @since 2.0 + */ +class Connection extends \yii\base\ApplicationComponent +{ + /** + * @var string The Data Source Name, or DSN, contains the information required to connect to the database. + * @see http://www.php.net/manual/en/function.PDO-construct.php + * + * Note that if your database is using GBK or BIG5 charset, we highly recommend you + * to upgrade to PHP 5.3.6+ and specify charset via DSN like the following to prevent + * from hacking: `mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;`. + */ + public $dsn; + /** + * @var string the username for establishing DB connection. Defaults to empty string. + */ + public $username = ''; + /** + * @var string the password for establishing DB connection. Defaults to empty string. + */ + public $password = ''; + /** + * @var integer number of seconds that table metadata can remain valid in cache. + * Use 0 or negative value to indicate not caching schema. + * If greater than 0 and the primary cache is enabled, the table metadata will be cached. + * @see schemaCachingExclude + */ + public $schemaCachingDuration = 0; + /** + * @var array list of tables whose metadata should NOT be cached. Defaults to empty array. + * @see schemaCachingDuration + */ + public $schemaCachingExclude = array(); + /** + * @var string the ID of the cache application component that is used to cache the table metadata. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching table metadata. + * @since 1.0.10 + */ + public $schemaCacheID = 'cache'; + /** + * @var integer number of seconds that query results can remain valid in cache. + * Use 0 or negative value to indicate not caching query results (the default behavior). + * + * In order to enable query caching, this property must be a positive + * integer and {@link queryCacheID} must point to a valid cache component ID. + * + * The method {@link cache()} is provided as a convenient way of setting this property + * and {@link queryCachingDependency} on the fly. + * + * @see cache + * @see queryCachingDependency + * @see queryCacheID + * @since 1.1.7 + */ + public $queryCachingDuration = 0; + /** + * @var CCacheDependency the dependency that will be used when saving query results into cache. + * @see queryCachingDuration + * @since 1.1.7 + */ + public $queryCachingDependency; + /** + * @var integer the number of SQL statements that need to be cached next. + * If this is 0, then even if query caching is enabled, no query will be cached. + * Note that each time after executing a SQL statement (whether executed on DB server or fetched from + * query cache), this property will be reduced by 1 until 0. + * @since 1.1.7 + */ + public $queryCachingCount = 0; + /** + * @var string the ID of the cache application component that is used for query caching. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable query caching. + * @since 1.1.7 + */ + public $queryCacheID = 'cache'; + /** + * @var boolean whether the database connection should be automatically established + * the component is being initialized. Defaults to true. Note, this property is only + * effective when the Connection object is used as an application component. + */ + public $autoConnect = true; + /** + * @var string the charset used for database connection. The property is only used + * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset + * as specified by the database. + * + * Note that if you're using GBK or BIG5 then it's highly recommended to + * update to PHP 5.3.6+ and to specify charset via DSN like + * 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'. + */ + public $charset; + /** + * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO + * will use the native prepare support if available. For some databases (such as MySQL), + * this may need to be set true so that PDO can emulate the prepare support to bypass + * the buggy native prepare support. Note, this property is only effective for PHP 5.1.3 or above. + * The default value is null, which will not change the ATTR_EMULATE_PREPARES value of PDO. + */ + public $emulatePrepare; + /** + * @var boolean whether to log the values that are bound to a prepare SQL statement. + * Defaults to false. During development, you may consider setting this property to true + * so that parameter values bound to SQL statements are logged for debugging purpose. + * You should be aware that logging parameter values could be expensive and have significant + * impact on the performance of your application. + * @since 1.0.5 + */ + public $enableParamLogging = false; + /** + * @var boolean whether to enable profiling the SQL statements being executed. + * Defaults to false. This should be mainly enabled and used during development + * to find out the bottleneck of SQL executions. + * @since 1.0.6 + */ + public $enableProfiling = false; + /** + * @var string the default prefix for table names. Defaults to null, meaning no table prefix. + * By setting this property, any token like '{{tableName}}' in {@link CDbCommand::text} will + * be replaced by 'prefixTableName', where 'prefix' refers to this property value. + * @since 1.1.0 + */ + public $tablePrefix; + /** + * @var array list of SQL statements that should be executed right after the DB connection is established. + * @since 1.1.1 + */ + public $initSQLs; + /** + * @var array mapping between PDO driver and schema class name. + * A schema class can be specified using path alias. + * @since 1.1.6 + */ + public $driverMap = array( + 'pgsql' => 'CPgsqlSchema', // PostgreSQL + 'mysqli' => 'CMysqlSchema', // MySQL + 'mysql' => 'CMysqlSchema', // MySQL + 'sqlite' => 'CSqliteSchema', // sqlite 3 + 'sqlite2' => 'CSqliteSchema', // sqlite 2 + 'mssql' => 'CMssqlSchema', // Mssql driver on windows hosts + 'dblib' => 'CMssqlSchema', // dblib drivers on linux (and maybe others os) hosts + 'sqlsrv' => 'CMssqlSchema', // Mssql + 'oci' => 'COciSchema', // Oracle driver + ); + + /** + * @var string Custom PDO wrapper class. + * @since 1.1.8 + */ + public $pdoClass = 'PDO'; + + private $_attributes = array(); + private $_active = false; + private $_pdo; + private $_transaction; + private $_schema; + + + /** + * Constructor. + * Note, the DB connection is not established when this connection + * instance is created. Set {@link setActive active} property to true + * to establish the connection. + * @param string $dsn The Data Source Name, or DSN, contains the information required to connect to the database. + * @param string $username The user name for the DSN string. + * @param string $password The password for the DSN string. + * @see http://www.php.net/manual/en/function.PDO-construct.php + */ + public function __construct($dsn = '', $username = '', $password = '') + { + $this->connectionString = $dsn; + $this->username = $username; + $this->password = $password; + } + + /** + * Close the connection when serializing. + * @return array + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a list of available PDO drivers. + * @return array list of available PDO drivers + * @see http://www.php.net/manual/en/function.PDO-getAvailableDrivers.php + */ + public static function getAvailableDrivers() + { + return PDO::getAvailableDrivers(); + } + + /** + * Initializes the component. + * This method is required by {@link IApplicationComponent} and is invoked by application + * when the Connection is used as an application component. + * If you override this method, make sure to call the parent implementation + * so that the component can be marked as initialized. + */ + public function init() + { + parent::init(); + if ($this->autoConnect) + $this->setActive(true); + } + + /** + * Returns whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getActive() + { + return $this->_active; + } + + /** + * Open or close the DB connection. + * @param boolean $value whether to open or close DB connection + * @throws CException if connection fails + */ + public function setActive($value) + { + if ($value != $this->_active) + { + if ($value) + $this->open(); + else + $this->close(); + } + } + + /** + * Sets the parameters about query caching. + * This method can be used to enable or disable query caching. + * By setting the $duration parameter to be 0, the query caching will be disabled. + * Otherwise, query results of the new SQL statements executed next will be saved in cache + * and remain valid for the specified duration. + * If the same query is executed again, the result may be fetched from cache directly + * without actually executing the SQL statement. + * @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 Connection the connection instance itself. + * @since 1.1.7 + */ + public function cache($duration, $dependency = null, $queryCount = 1) + { + $this->queryCachingDuration = $duration; + $this->queryCachingDependency = $dependency; + $this->queryCachingCount = $queryCount; + return $this; + } + + /** + * Opens DB connection if it is currently not + * @throws CException if connection fails + */ + protected function open() + { + if ($this->_pdo === null) + { + if (empty($this->connectionString)) + throw new CDbException(Yii::t('yii', 'Connection.connectionString cannot be empty.')); + try + { + Yii::trace('Opening DB connection', 'system.db.Connection'); + $this->_pdo = $this->createPdoInstance(); + $this->initConnection($this->_pdo); + $this->_active = true; + } + catch(PDOException $e) + { + if (YII_DEBUG) + { + throw new CDbException(Yii::t('yii', 'Connection failed to open the DB connection: {error}', + array('{error}' => $e->getMessage())), (int)$e->getCode(), $e->errorInfo); + } + else + { + Yii::log($e->getMessage(), CLogger::LEVEL_ERROR, 'exception.CDbException'); + throw new CDbException(Yii::t('yii', 'Connection failed to open the DB connection.'), (int)$e->getCode(), $e->errorInfo); + } + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + protected function close() + { + Yii::trace('Closing DB connection', 'system.db.Connection'); + $this->_pdo = null; + $this->_active = false; + $this->_schema = null; + } + + /** + * Creates the PDO instance. + * When some functionalities are missing in the pdo driver, we may use + * an adapter class to provides them. + * @return PDO the pdo instance + * @since 1.0.4 + */ + protected function createPdoInstance() + { + $pdoClass = $this->pdoClass; + if (($pos = strpos($this->connectionString, ':')) !== false) + { + $driver = strtolower(substr($this->connectionString, 0, $pos)); + if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') + $pdoClass = 'CMssqlPdoAdapter'; + } + return new $pdoClass($this->connectionString, $this->username, + $this->password, $this->_attributes); + } + + /** + * Initializes the open db connection. + * This method is invoked right after the db connection is established. + * The default implementation is to set the charset for MySQL and PostgreSQL database connections. + * @param PDO $pdo the PDO instance + */ + protected function initConnection($pdo) + { + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); + if ($this->charset !== null) + { + $driver = strtolower($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); + if (in_array($driver, array('pgsql', 'mysql', 'mysqli'))) + $pdo->exec('SET NAMES ' . $pdo->quote($this->charset)); + } + if ($this->initSQLs !== null) + { + foreach ($this->initSQLs as $sql) + $pdo->exec($sql); + } + } + + /** + * Returns the PDO instance. + * @return PDO the PDO instance, null if the connection is not established yet + */ + public function getPdoInstance() + { + return $this->_pdo; + } + + /** + * Creates a command for execution. + * @param mixed $query the DB query to be executed. This can be either a string representing a SQL statement, + * or an array representing different fragments of a SQL statement. Please refer to {@link CDbCommand::__construct} + * for more details about how to pass an array as the query. If this parameter is not given, + * you will have to call query builder methods of {@link CDbCommand} to build the DB query. + * @return CDbCommand the DB command + */ + public function createCommand($query = null) + { + $this->setActive(true); + return new CDbCommand($this, $query); + } + + /** + * Returns the currently active transaction. + * @return CDbTransaction the currently active transaction. Null if no active transaction. + */ + public function getCurrentTransaction() + { + if ($this->_transaction !== null) + { + if ($this->_transaction->getActive()) + return $this->_transaction; + } + return null; + } + + /** + * Starts a transaction. + * @return CDbTransaction the transaction initiated + */ + public function beginTransaction() + { + Yii::trace('Starting transaction', 'system.db.Connection'); + $this->setActive(true); + $this->_pdo->beginTransaction(); + return $this->_transaction = new CDbTransaction($this); + } + + /** + * Returns the database schema for the current connection + * @return CDbSchema the database schema for the current connection + */ + public function getSchema() + { + if ($this->_schema !== null) + return $this->_schema; + else + { + $driver = $this->getDriverName(); + if (isset($this->driverMap[$driver])) + return $this->_schema = Yii::createComponent($this->driverMap[$driver], $this); + else + throw new CDbException(Yii::t('yii', 'Connection does not support reading schema for {driver} database.', + array('{driver}' => $driver))); + } + } + + /** + * Returns the SQL command builder for the current DB connection. + * @return CDbCommandBuilder the command builder + * @since 1.0.4 + */ + public function getCommandBuilder() + { + return $this->getSchema()->getCommandBuilder(); + } + + /** + * Returns the ID of the last inserted row or sequence value. + * @param string $sequenceName name of the sequence object (required by some DBMS) + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php + */ + public function getLastInsertID($sequenceName = '') + { + $this->setActive(true); + return $this->_pdo->lastInsertId($sequenceName); + } + + /** + * Quotes a string value for use in a query. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if (is_int($str) || is_float($str)) + return $str; + + $this->setActive(true); + if (($value = $this->_pdo->quote($str)) !== false) + return $value; + else // the driver doesn't support quote (e.g. oci) + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } + + /** + * Quotes a table name for use in a query. + * If the table name contains schema prefix, the prefix will also be properly quoted. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return $this->getSchema()->quoteTableName($name); + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return $this->getSchema()->quoteColumnName($name); + } + + /** + * Determines the PDO type for the specified PHP type. + * @param string $type The PHP type (obtained by gettype() call). + * @return integer the corresponding PDO type + */ + public function getPdoType($type) + { + static $map = array + ( + 'boolean' => PDO::PARAM_BOOL, + 'integer' => PDO::PARAM_INT, + 'string' => PDO::PARAM_STR, + 'NULL' => PDO::PARAM_NULL, + ); + return isset($map[$type]) ? $map[$type] : PDO::PARAM_STR; + } + + /** + * Returns the case of the column names + * @return mixed the case of the column names + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function getColumnCase() + { + return $this->getAttribute(PDO::ATTR_CASE); + } + + /** + * Sets the case of the column names. + * @param mixed $value the case of the column names + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function setColumnCase($value) + { + $this->setAttribute(PDO::ATTR_CASE, $value); + } + + /** + * Returns how the null and empty strings are converted. + * @return mixed how the null and empty strings are converted + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function getNullConversion() + { + return $this->getAttribute(PDO::ATTR_ORACLE_NULLS); + } + + /** + * Sets how the null and empty strings are converted. + * @param mixed $value how the null and empty strings are converted + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function setNullConversion($value) + { + $this->setAttribute(PDO::ATTR_ORACLE_NULLS, $value); + } + + /** + * Returns whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + * @return boolean whether creating or updating a DB record will be automatically committed. + */ + public function getAutoCommit() + { + return $this->getAttribute(PDO::ATTR_AUTOCOMMIT); + } + + /** + * Sets whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + * @param boolean $value whether creating or updating a DB record will be automatically committed. + */ + public function setAutoCommit($value) + { + $this->setAttribute(PDO::ATTR_AUTOCOMMIT, $value); + } + + /** + * Returns whether the connection is persistent or not. + * Some DBMS (such as sqlite) may not support this feature. + * @return boolean whether the connection is persistent or not + */ + public function getPersistent() + { + return $this->getAttribute(PDO::ATTR_PERSISTENT); + } + + /** + * Sets whether the connection is persistent or not. + * Some DBMS (such as sqlite) may not support this feature. + * @param boolean $value whether the connection is persistent or not + */ + public function setPersistent($value) + { + return $this->setAttribute(PDO::ATTR_PERSISTENT, $value); + } + + /** + * Returns the name of the DB driver + * @return string name of the DB driver + */ + public function getDriverName() + { + if (($pos = strpos($this->connectionString, ':')) !== false) + return strtolower(substr($this->connectionString, 0, $pos)); + // return $this->getAttribute(PDO::ATTR_DRIVER_NAME); + } + + /** + * Returns the version information of the DB driver. + * @return string the version information of the DB driver + */ + public function getClientVersion() + { + return $this->getAttribute(PDO::ATTR_CLIENT_VERSION); + } + + /** + * Returns the status of the connection. + * Some DBMS (such as sqlite) may not support this feature. + * @return string the status of the connection + */ + public function getConnectionStatus() + { + return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS); + } + + /** + * Returns whether the connection performs data prefetching. + * @return boolean whether the connection performs data prefetching + */ + public function getPrefetch() + { + return $this->getAttribute(PDO::ATTR_PREFETCH); + } + + /** + * Returns the information of DBMS server. + * @return string the information of DBMS server + */ + public function getServerInfo() + { + return $this->getAttribute(PDO::ATTR_SERVER_INFO); + } + + /** + * Returns the version information of DBMS server. + * @return string the version information of DBMS server + */ + public function getServerVersion() + { + return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Returns the timeout settings for the connection. + * @return integer timeout settings for the connection + */ + public function getTimeout() + { + return $this->getAttribute(PDO::ATTR_TIMEOUT); + } + + /** + * Obtains a specific DB connection attribute information. + * @param integer $name the attribute to be queried + * @return mixed the corresponding attribute information + * @see http://www.php.net/manual/en/function.PDO-getAttribute.php + */ + public function getAttribute($name) + { + $this->setActive(true); + return $this->_pdo->getAttribute($name); + } + + /** + * Sets an attribute on the database connection. + * @param integer $name the attribute to be set + * @param mixed $value the attribute value + * @see http://www.php.net/manual/en/function.PDO-setAttribute.php + */ + public function setAttribute($name, $value) + { + if ($this->_pdo instanceof PDO) + $this->_pdo->setAttribute($name, $value); + else + $this->_attributes[$name] = $value; + } + + /** + * Returns the attributes that are previously explicitly set for the DB connection. + * @return array attributes (name=>value) that are previously explicitly set for the DB connection. + * @see setAttributes + * @since 1.1.7 + */ + public function getAttributes() + { + return $this->_attributes; + } + + /** + * Sets a set of attributes on the database connection. + * @param array $values attributes (name=>value) to be set. + * @see setAttribute + * @since 1.1.7 + */ + public function setAttributes($values) + { + foreach ($values as $name => $value) + $this->_attributes[$name] = $value; + } + + /** + * Returns the statistical results of SQL executions. + * The results returned include the number of SQL statements executed and + * the total time spent. + * In order to use this method, {@link enableProfiling} has to be set true. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + * @since 1.0.6 + */ + public function getStats() + { + $logger = Yii::getLogger(); + $timings = $logger->getProfilingResults(null, 'system.db.CDbCommand.query'); + $count = count($timings); + $time = array_sum($timings); + $timings = $logger->getProfilingResults(null, 'system.db.CDbCommand.execute'); + $count += count($timings); + $time += array_sum($timings); + return array($count, $time); + } +} diff --git a/framework/db/pdo/DataReader.php b/framework/db/pdo/DataReader.php new file mode 100644 index 0000000..b62a08b --- /dev/null +++ b/framework/db/pdo/DataReader.php @@ -0,0 +1,241 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbDataReader represents a forward-only stream of rows from a query result set. + * + * To read the current row of data, call {@link read}. The method {@link readAll} + * returns all the rows in a single array. + * + * One can also retrieve the rows of data in CDbDataReader by using foreach: + *
+ * foreach($reader as $row)
+ *     // $row represents a row of data
+ * 
+ * Since CDbDataReader is a forward-only stream, you can only traverse it once. + * + * It is possible to use a specific mode of data fetching by setting + * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php} + * for more details. + * + * @author Qiang Xue + * @version $Id: CDbDataReader.php 3204 2011-05-05 21:36:32Z alexander.makarow $ + * @package system.db + * @since 1.0 + */ +class CDbDataReader extends CComponent implements Iterator, Countable +{ + private $_statement; + private $_closed = false; + private $_row; + private $_index = -1; + + /** + * Constructor. + * @param CDbCommand $command the command generating the query result + */ + public function __construct(CDbCommand $command) + { + $this->_statement = $command->getPdoStatement(); + $this->_statement->setFetchMode(PDO::FETCH_ASSOC); + } + + /** + * Binds a column to a PHP variable. + * When rows of data are being fetched, the corresponding column value + * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. + * @param mixed $column Number of the column (1-indexed) or name of the column + * in the result set. If using the column name, be aware that the name + * should match the case of the column, as returned by the driver. + * @param mixed $value Name of the PHP variable to which the column will be bound. + * @param integer $dataType Data type of the parameter + * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php + */ + public function bindColumn($column, &$value, $dataType = null) + { + if ($dataType === null) + $this->_statement->bindColumn($column, $value); + else + $this->_statement->bindColumn($column, $value, $dataType); + } + + /** + * Set the default fetch mode for this statement + * @param mixed $mode fetch mode + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + */ + public function setFetchMode($mode) + { + $params = func_get_args(); + call_user_func_array(array($this->_statement, 'setFetchMode'), $params); + } + + /** + * Advances the reader to the next row in a result set. + * @return array|false the current row, false if no more row available + */ + public function read() + { + return $this->_statement->fetch(); + } + + /** + * Returns a single column from the next row of a result set. + * @param integer $columnIndex zero-based column index + * @return mixed|false the column of the current row, false if no more row available + */ + public function readColumn($columnIndex) + { + return $this->_statement->fetchColumn($columnIndex); + } + + /** + * Returns an object populated with the next row of data. + * @param string $className class name of the object to be created and populated + * @param array $fields Elements of this array are passed to the constructor + * @return mixed|false the populated object, false if no more row of data available + */ + public function readObject($className, $fields) + { + return $this->_statement->fetchObject($className, $fields); + } + + /** + * Reads the whole result set into an array. + * @return array the result set (each array element represents a row of data). + * An empty array will be returned if the result contains no row. + */ + public function readAll() + { + return $this->_statement->fetchAll(); + } + + /** + * Advances the reader to the next result when reading the results of a batch of statements. + * This method is only useful when there are multiple result sets + * returned by the query. Not all DBMS support this feature. + * @return boolean Returns true on success or false on failure. + */ + public function nextResult() + { + if (($result = $this->_statement->nextRowset()) !== false) + $this->_index = -1; + return $result; + } + + /** + * Closes the reader. + * This frees up the resources allocated for executing this SQL statement. + * Read attemps after this method call are unpredictable. + */ + public function close() + { + $this->_statement->closeCursor(); + $this->_closed = true; + } + + /** + * whether the reader is closed or not. + * @return boolean whether the reader is closed or not. + */ + public function getIsClosed() + { + return $this->_closed; + } + + /** + * Returns the number of rows in the result set. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function getRowCount() + { + return $this->_statement->rowCount(); + } + + /** + * Returns the number of rows in the result set. + * This method is required by the Countable interface. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function count() + { + return $this->getRowCount(); + } + + /** + * Returns the number of columns in the result set. + * Note, even there's no row in the reader, this still gives correct column number. + * @return integer the number of columns in the result set. + */ + public function getColumnCount() + { + return $this->_statement->columnCount(); + } + + /** + * Resets the iterator to the initial state. + * This method is required by the interface Iterator. + * @throws CException if this method is invoked twice + */ + public function rewind() + { + if ($this->_index < 0) + { + $this->_row = $this->_statement->fetch(); + $this->_index = 0; + } + else + throw new CDbException(Yii::t('yii', 'CDbDataReader cannot rewind. It is a forward-only reader.')); + } + + /** + * Returns the index of the current row. + * This method is required by the interface Iterator. + * @return integer the index of the current row. + */ + public function key() + { + return $this->_index; + } + + /** + * Returns the current row. + * This method is required by the interface Iterator. + * @return mixed the current row. + */ + public function current() + { + return $this->_row; + } + + /** + * Moves the internal pointer to the next row. + * This method is required by the interface Iterator. + */ + public function next() + { + $this->_row = $this->_statement->fetch(); + $this->_index++; + } + + /** + * Returns whether there is a row of data at current position. + * This method is required by the interface Iterator. + * @return boolean whether there is a row of data at current position. + */ + public function valid() + { + return $this->_row !== false; + } +} diff --git a/framework/db/pdo/Transaction.php b/framework/db/pdo/Transaction.php new file mode 100644 index 0000000..0d6a9c6 --- /dev/null +++ b/framework/db/pdo/Transaction.php @@ -0,0 +1,108 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbTransaction represents a DB transaction. + * + * It is usually created by calling {@link CDbConnection::beginTransaction}. + * + * The following code is a common scenario of using transactions: + *
+ * $transaction=$connection->beginTransaction();
+ * try
+ * {
+ *    $connection->createCommand($sql1)->execute();
+ *    $connection->createCommand($sql2)->execute();
+ *    //.... other SQL executions
+ *    $transaction->commit();
+ * }
+ * catch(Exception $e)
+ * {
+ *    $transaction->rollBack();
+ * }
+ * 
+ * + * @author Qiang Xue + * @version $Id: CDbTransaction.php 3069 2011-03-14 00:28:38Z qiang.xue $ + * @package system.db + * @since 1.0 + */ +class CDbTransaction extends CComponent +{ + private $_connection = null; + private $_active; + + /** + * Constructor. + * @param CDbConnection $connection the connection associated with this transaction + * @see CDbConnection::beginTransaction + */ + public function __construct(CDbConnection $connection) + { + $this->_connection = $connection; + $this->_active = true; + } + + /** + * Commits a transaction. + * @throws CException if the transaction or the DB connection is not active. + */ + public function commit() + { + if ($this->_active && $this->_connection->getActive()) + { + Yii::trace('Committing transaction', 'system.db.CDbTransaction'); + $this->_connection->getPdoInstance()->commit(); + $this->_active = false; + } + else + throw new CDbException(Yii::t('yii', 'CDbTransaction is inactive and cannot perform commit or roll back operations.')); + } + + /** + * Rolls back a transaction. + * @throws CException if the transaction or the DB connection is not active. + */ + public function rollback() + { + if ($this->_active && $this->_connection->getActive()) + { + Yii::trace('Rolling back transaction', 'system.db.CDbTransaction'); + $this->_connection->getPdoInstance()->rollBack(); + $this->_active = false; + } + else + throw new CDbException(Yii::t('yii', 'CDbTransaction is inactive and cannot perform commit or roll back operations.')); + } + + /** + * @return CDbConnection the DB connection for this transaction + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return boolean whether this transaction is active + */ + public function getActive() + { + return $this->_active; + } + + /** + * @param boolean $value whether this transaction is active + */ + protected function setActive($value) + { + $this->_active = $value; + } +}