ActiveRecord.php 30.6 KB
Newer Older
w  
Qiang Xue committed
1 2
<?php
/**
w  
Qiang Xue committed
3
 * ActiveRecord class file.
w  
Qiang Xue committed
4 5 6
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
7
 * @copyright Copyright &copy; 2008-2012 Yii Software LLC
w  
Qiang Xue committed
8 9 10
 * @license http://www.yiiframework.com/license/
 */

w  
Qiang Xue committed
11 12
namespace yii\db\ar;

Qiang Xue committed
13 14 15
use yii\base\Model;
use yii\base\Event;
use yii\base\ModelEvent;
Qiang Xue committed
16 17 18 19
use yii\db\Exception;
use yii\db\dao\Connection;
use yii\db\dao\TableSchema;
use yii\db\dao\Query;
Qiang Xue committed
20
use yii\db\dao\Expression;
Qiang Xue committed
21
use yii\util\StringHelper;
w  
Qiang Xue committed
22

w  
Qiang Xue committed
23
/**
w  
Qiang Xue committed
24
 * ActiveRecord is the base class for classes representing relational data.
w  
Qiang Xue committed
25 26
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
Alexander Makarov committed
27
 * @since 2.0
w  
Qiang Xue committed
28
 *
29
 * @property array $attributes attribute values indexed by attribute names
30
 *
31 32 33
 * ActiveRecord provides a set of events for further customization:
 *
 * - `beforeInsert`. Raised before the record is saved.
34
 *   By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[save()]] will be stopped.
35 36
 * - `afterInsert`. Raised after the record is saved.
 * - `beforeUpdate`. Raised before the record is saved.
37
 *   By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[save()]] will be stopped.
38 39
 * - `afterUpdate`. Raised after the record is saved.
 * - `beforeDelete`. Raised before the record is deleted.
40
 *   By setting [[\yii\base\ModelEvent::isValid]] to be false, the normal [[delete()]] process will be stopped.
41
 * - `afterDelete`. Raised after the record is deleted.
42
 *
w  
Qiang Xue committed
43
 */
Qiang Xue committed
44
abstract class ActiveRecord extends Model
w  
Qiang Xue committed
45
{
46 47 48 49
	/**
	 * @var ActiveRecord[] global model instances indexed by model class names
	 */
	private static $_models = array();
w  
Qiang Xue committed
50
	/**
Qiang Xue committed
51 52 53 54 55
	 * @var array attribute values indexed by attribute names
	 */
	private $_attributes = array();
	/**
	 * @var array old attribute values indexed by attribute names.
w  
Qiang Xue committed
56
	 */
Qiang Xue committed
57
	private $_oldAttributes;
w  
Qiang Xue committed
58

59 60 61 62 63 64 65 66

	/**
	 * Returns a model instance to support accessing non-static methods such as [[table()]], [[primaryKey()]].
	 * @return ActiveRecord
	 */
	public static function model()
	{
		$className = get_called_class();
Qiang Xue committed
67 68
		if (!isset(self::$_models[$className])) {
			self::$_models[$className] = new static;
69
		}
Qiang Xue committed
70
		return self::$_models[$className];
71 72
	}

Qiang Xue committed
73 74 75 76 77 78 79 80 81 82 83
	/**
	 * Returns the database connection used by this AR class.
	 * By default, the "db" application component is used as the database connection.
	 * You may override this method if you want to use a different database connection.
	 * @return Connection the database connection used by this AR class.
	 */
	public static function getDbConnection()
	{
		return \Yii::$application->getDb();
	}

Qiang Xue committed
84
	/**
Qiang Xue committed
85
	 * Creates an [[ActiveQuery]] instance for query purpose.
Qiang Xue committed
86
	 *
Qiang Xue committed
87 88
	 * Because [[ActiveQuery]] implements a set of query building methods,
	 * additional query conditions can be specified by calling the methods of [[ActiveQuery]].
Qiang Xue committed
89 90 91 92 93 94
	 *
	 * Below are some usage examples:
	 *
	 * ~~~
	 * // find all customers
	 * $customers = Customer::find()->all();
Qiang Xue committed
95
	 * // find a single customer whose primary key value is 10
Qiang Xue committed
96 97 98
	 * $customer = Customer::find(10);
	 * // the above is equivalent to:
	 * Customer::find()->where(array('id' => 10))->one();
Qiang Xue committed
99
	 * // find all active customers and order them by their age:
Qiang Xue committed
100 101
	 * $customers = Customer::find()
	 *     ->where(array('status' => 1))
Qiang Xue committed
102
	 *     ->orderBy('age')
Qiang Xue committed
103 104 105 106
	 *     ->all();
	 * // or alternatively:
	 * $customers = Customer::find(array(
	 *     'where' => array('status' => 1),
Qiang Xue committed
107
	 *     'orderBy' => 'age',
Qiang Xue committed
108
	 * ))->all();
Qiang Xue committed
109 110 111 112
	 * ~~~
	 *
	 * @param mixed $q the query parameter. This can be one of the followings:
	 *
Qiang Xue committed
113 114
	 *  - a scalar value (integer or string): query by a single primary key value.
	 *  - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object.
Qiang Xue committed
115
	 *
Qiang Xue committed
116 117 118
	 * @return ActiveQuery|ActiveRecord|null the [[ActiveQuery]] instance for query purpose, or
	 * the ActiveRecord object when a scalar is passed to this method which is considered to be a
	 * primary key value (null will be returned if no record is found in this case.)
Qiang Xue committed
119 120 121
	 */
	public static function find($q = null)
	{
Qiang Xue committed
122 123 124 125 126
		$query = static::createActiveQuery();
		if (is_array($q)) {
			foreach ($q as $name => $value) {
				$query->$name = $value;
			}
Qiang Xue committed
127 128
		} elseif ($q !== null) {
			// query by primary key
Qiang Xue committed
129
			$primaryKey = static::model()->primaryKey();
Qiang Xue committed
130
			return $query->where(array($primaryKey[0] => $q))->one();
Qiang Xue committed
131
		}
Qiang Xue committed
132
		return $query;
w  
Qiang Xue committed
133 134
	}

Qiang Xue committed
135
	/**
Qiang Xue committed
136
	 * Creates an [[ActiveQuery]] instance and queries by a given SQL statement.
Qiang Xue committed
137
	 * Note that because the SQL statement is already specified, calling further
138
	 * query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect.
Qiang Xue committed
139
	 * Methods such as `with()`, `asArray()` can still be called though.
Qiang Xue committed
140 141
	 * @param string $sql the SQL statement to be executed
	 * @param array $params parameters to be bound to the SQL statement during execution.
Qiang Xue committed
142
	 * @return ActiveQuery the [[ActiveQuery]] instance
Qiang Xue committed
143
	 */
Qiang Xue committed
144
	public static function findBySql($sql, $params = array())
w  
Qiang Xue committed
145
	{
Qiang Xue committed
146 147 148 149 150 151 152 153 154 155 156 157
		$query = static::createActiveQuery();
		$query->sql = $sql;
		return $query->params($params);
	}

	/**
	 * Performs a COUNT query for this AR class.
	 *
	 * Below are some usage examples:
	 *
	 * ~~~
	 * // count the total number of customers
Qiang Xue committed
158
	 * echo Customer::count()->value();
Qiang Xue committed
159 160 161
	 * // count the number of active customers:
	 * echo Customer::count(array(
	 *     'where' => array('status' => 1),
Qiang Xue committed
162 163 164 165 166 167 168
	 * ))->value();
	 * // equivalent usage:
	 * echo Customer::count()
	 *     ->where(array('status' => 1))
	 *     ->value();
	 * // customize the count option
	 * echo Customer::count('COUNT(DISTINCT age)')->value();
Qiang Xue committed
169 170
	 * ~~~
	 *
Qiang Xue committed
171
	 * @param array|string $q the query option. This can be one of the followings:
Qiang Xue committed
172
	 *
Qiang Xue committed
173 174 175 176
	 *  - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object.
	 *  - a string: the count expression, e.g. 'COUNT(DISTINCT age)'.
	 *
	 * @return ActiveQuery the [[ActiveQuery]] instance
Qiang Xue committed
177 178 179 180 181 182 183 184
	 */
	public static function count($q = null)
	{
		$query = static::createActiveQuery();
		if (is_array($q)) {
			foreach ($q as $name => $value) {
				$query->$name = $value;
			}
Qiang Xue committed
185 186 187
		} elseif ($q !== null) {
			$query->select = array($q);
		} elseif ($query->select === null) {
Qiang Xue committed
188
			$query->select = array('COUNT(*)');
Qiang Xue committed
189
		}
Qiang Xue committed
190
		return $query;
w  
Qiang Xue committed
191 192
	}

Qiang Xue committed
193 194 195 196 197 198 199 200
	/**
	 * Updates the whole table using the provided attribute values and conditions.
	 * @param array $attributes attribute values to be saved into the table
	 * @param string|array $condition the conditions that will be put in the WHERE part.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name=>value) to be bound to the query.
	 * @return integer the number of rows updated
	 */
Qiang Xue committed
201
	public static function updateAll($attributes, $condition = '', $params = array())
w  
Qiang Xue committed
202
	{
Qiang Xue committed
203
		$query = new Query;
204
		$query->update(static::model()->tableName(), $attributes, $condition, $params);
Qiang Xue committed
205
		return $query->createCommand(static::getDbConnection())->execute();
w  
Qiang Xue committed
206 207
	}

Qiang Xue committed
208 209 210 211 212 213 214 215 216
	/**
	 * Updates the whole table using the provided counter values and conditions.
	 * @param array $counters the counters to be updated (attribute name => increment value).
	 * @param string|array $condition the conditions that will be put in the WHERE part.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name=>value) to be bound to the query.
	 * @return integer the number of rows updated
	 */
	public static function updateAllCounters($counters, $condition = '', $params = array())
w  
Qiang Xue committed
217
	{
Qiang Xue committed
218
		$db = static::getDbConnection();
Qiang Xue committed
219 220 221 222 223 224
		foreach ($counters as $name => $value) {
			$value = (int)$value;
			$quotedName = $db->quoteColumnName($name, true);
			$counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value");
		}
		$query = new Query;
225
		$query->update(static::model()->tableName(), $counters, $condition, $params);
Qiang Xue committed
226
		return $query->createCommand($db)->execute();
w  
Qiang Xue committed
227 228
	}

Qiang Xue committed
229 230 231 232 233 234 235
	/**
	 * Deletes rows in the table using the provided conditions.
	 * @param string|array $condition the conditions that will be put in the WHERE part.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name=>value) to be bound to the query.
	 * @return integer the number of rows updated
	 */
Qiang Xue committed
236
	public static function deleteAll($condition = '', $params = array())
w  
Qiang Xue committed
237
	{
Qiang Xue committed
238
		$query = new Query;
239
		$query->delete(static::model()->tableName(), $condition, $params);
Qiang Xue committed
240
		return $query->createCommand(static::getDbConnection())->execute();
w  
Qiang Xue committed
241 242
	}

.  
Qiang Xue committed
243
	/**
Qiang Xue committed
244 245 246
	 * Creates a [[ActiveQuery]] instance.
	 * This method is called by [[find()]] and [[findBySql()]] to start a SELECT query.
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance.
.  
Qiang Xue committed
247
	 */
Qiang Xue committed
248
	public static function createActiveQuery()
w  
Qiang Xue committed
249
	{
Qiang Xue committed
250
		return new ActiveQuery(array('modelClass' => get_called_class()));
w  
Qiang Xue committed
251 252 253
	}

	/**
Qiang Xue committed
254
	 * Declares the name of the database table associated with this AR class.
Qiang Xue committed
255
	 * By default this method returns the class name as the table name by calling [[StringHelper::camel2id()]].
Qiang Xue committed
256
	 * For example, 'Customer' becomes 'customer', and 'OrderDetail' becomes 'order_detail'.
w  
Qiang Xue committed
257 258 259
	 * You may override this method if the table is not named after this convention.
	 * @return string the table name
	 */
Qiang Xue committed
260
	public static function tableName()
w  
Qiang Xue committed
261
	{
Qiang Xue committed
262
		return StringHelper::camel2id(basename(get_called_class()), '_');
w  
Qiang Xue committed
263 264 265
	}

	/**
Qiang Xue committed
266 267
	 * Returns the schema information of the DB table associated with this AR class.
	 * @return TableSchema the schema information of the DB table associated with this AR class.
w  
Qiang Xue committed
268
	 */
Qiang Xue committed
269
	public static function getTableSchema()
w  
Qiang Xue committed
270
	{
Qiang Xue committed
271
		return static::getDbConnection()->getTableSchema(static::tableName());
w  
Qiang Xue committed
272 273 274
	}

	/**
Qiang Xue committed
275 276 277 278 279 280 281
	 * Returns the primary keys for this AR class.
	 * The default implementation will return the primary keys as declared
	 * in the DB table that is associated with this AR class.
	 * If the DB table does not declare any primary key, you should override
	 * this method to return the attributes that you want to use as primary keys
	 * for this AR class.
	 * @return string[] the primary keys of the associated database table.
w  
Qiang Xue committed
282
	 */
Qiang Xue committed
283
	public static function primaryKey()
w  
Qiang Xue committed
284
	{
Qiang Xue committed
285
		return static::getTableSchema()->primaryKey;
w  
Qiang Xue committed
286 287
	}

Qiang Xue committed
288 289
	/**
	 * Returns the default named scope that should be implicitly applied to all queries for this model.
Qiang Xue committed
290
	 * Note, the default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries.
Qiang Xue committed
291
	 * The default implementation simply returns an empty array. You may override this method
Qiang Xue committed
292 293
	 * if the model needs to be queried with some default criteria (e.g. only non-deleted users should be returned).
	 * @param ActiveQuery
Qiang Xue committed
294
	 */
Qiang Xue committed
295
	public static function defaultScope($query)
Qiang Xue committed
296
	{
Qiang Xue committed
297
		// todo: should we drop this?
Qiang Xue committed
298 299
	}

w  
Qiang Xue committed
300
	/**
Qiang Xue committed
301
	 * PHP getter magic method.
Qiang Xue committed
302
	 * This method is overridden so that attributes and related objects can be accessed like properties.
Qiang Xue committed
303 304 305 306 307 308
	 * @param string $name property name
	 * @return mixed property value
	 * @see getAttribute
	 */
	public function __get($name)
	{
Qiang Xue committed
309
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
Qiang Xue committed
310
			return $this->_attributes[$name];
Qiang Xue committed
311
		} elseif (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
312
			return null;
Qiang Xue committed
313
		} elseif (method_exists($this, $name)) {
Qiang Xue committed
314 315 316 317 318
			// lazy loading related records
			$query = $this->$name();
			return $this->_attributes[$name] = $query->multiple ? $query->all() : $query->one();
		} else {
			return parent::__get($name);
Qiang Xue committed
319 320 321 322 323 324 325 326 327 328 329
		}
	}

	/**
	 * 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)
	{
Qiang Xue committed
330
		if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) {
Qiang Xue committed
331 332 333 334 335 336 337 338 339 340 341 342 343 344
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
	 * This method overrides the parent implementation by checking
	 * if the named attribute is null or not.
	 * @param string $name the property name or the event name
	 * @return boolean whether the property value is null
	 */
	public function __isset($name)
w  
Qiang Xue committed
345
	{
Qiang Xue committed
346
		if (isset($this->_attributes[$name]) || isset($this->_related[$name])) {
Qiang Xue committed
347
			return true;
Qiang Xue committed
348
		}
Qiang Xue committed
349
		if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) {
Qiang Xue committed
350
			return false;
Qiang Xue committed
351
		} else {
Qiang Xue committed
352 353 354 355 356 357 358 359 360 361 362 363
			return parent::__isset($name);
		}
	}

	/**
	 * Sets a component property to be null.
	 * This method overrides the parent implementation by clearing
	 * the specified attribute value.
	 * @param string $name the property name or the event name
	 */
	public function __unset($name)
	{
Qiang Xue committed
364
		if (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
365
			unset($this->_attributes[$name]);
Qiang Xue committed
366
		} elseif (method_exists($this, $name)) {
Qiang Xue committed
367
			unset($this->_related[$name]);
Qiang Xue committed
368
		} else {
Qiang Xue committed
369 370 371 372
			parent::__unset($name);
		}
	}

Qiang Xue committed
373 374 375 376 377
	public function hasOne($class, $link)
	{
		return new HasOneRelation(array(
			'modelClass' => $class,
			'parentClass' => get_class($this),
Qiang Xue committed
378
			'parentRecords' => array($this),
Qiang Xue committed
379 380 381 382 383 384 385 386 387
			'link' => $link,
		));
	}

	public function hasMany($class, $link)
	{
		return new HasManyRelation(array(
			'modelClass' => $class,
			'parentClass' => get_class($this),
Qiang Xue committed
388
			'parentRecords' => array($this),
Qiang Xue committed
389 390 391 392 393 394 395 396 397
			'link' => $link,
		));
	}

	public function manyMany($class, $leftLink, $joinTable, $rightLink)
	{
		return new ManyManyRelation(array(
			'modelClass' => $class,
			'parentClass' => get_class($this),
Qiang Xue committed
398
			'parentRecords' => array($this),
Qiang Xue committed
399 400 401 402 403 404
			'leftLink' => $leftLink,
			'joinTable' => $joinTable,
			'rightLink' => $rightLink,
		));
	}

Qiang Xue committed
405 406
	/**
	 * Initializes the internal storage for the relation.
Qiang Xue committed
407
	 * This method is internally used by [[ActiveQuery]] when populating relation data.
Qiang Xue committed
408 409 410
	 * @param ActiveRelation $relation the relation object
	 */
	public function initRelation($relation)
Qiang Xue committed
411
	{
Qiang Xue committed
412
		$this->_related[$relation->name] = $relation->hasMany ? array() : null;
Qiang Xue committed
413 414
	}

Qiang Xue committed
415 416 417 418
	/**
	 * @param ActiveRelation $relation
	 * @param ActiveRecord $record
	 */
Qiang Xue committed
419 420 421
	public function addRelatedRecord($relation, $record)
	{
		if ($relation->hasMany) {
Qiang Xue committed
422 423
			if ($relation->index !== null) {
				$this->_related[$relation->name][$record->{$relation->index}] = $record;
Qiang Xue committed
424 425 426
			} else {
				$this->_related[$relation->name][] = $record;
			}
Qiang Xue committed
427
		} else {
Qiang Xue committed
428
			$this->_related[$relation->name] = $record;
Qiang Xue committed
429 430 431
		}
	}

Qiang Xue committed
432 433 434 435 436 437 438
	/**
	 * 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.
Qiang Xue committed
439
	 * @param ActiveRelation|string $relation the relation object or the name of the relation
440
	 * @param array|\Closure $params additional parameters that customize the query conditions as specified in the relation declaration.
Qiang Xue committed
441
	 * @return mixed the related object(s).
Qiang Xue committed
442
	 * @throws Exception if the relation is not specified in [[relations()]].
Qiang Xue committed
443
	 */
Qiang Xue committed
444
	public function findByRelation($relation, $params = array())
Qiang Xue committed
445
	{
Qiang Xue committed
446 447 448 449
		if (is_string($relation)) {
			$md = $this->getMetaData();
			if (!isset($md->relations[$relation])) {
				throw new Exception(get_class($this) . ' has no relation named "' . $relation . '".');
Qiang Xue committed
450
			}
Qiang Xue committed
451
			$relation = $md->relations[$relation];
Qiang Xue committed
452
		}
Qiang Xue committed
453
		$relation = clone $relation;
454
		if ($params instanceof \Closure) {
Qiang Xue committed
455
			$params($relation);
456 457 458 459
		} else {
			foreach ($params as $name => $value) {
				$relation->$name = $value;
			}
Qiang Xue committed
460 461 462
		}

		$finder = new ActiveFinder($this->getDbConnection());
Qiang Xue committed
463
		return $finder->findWithRecord($this, $relation);
Qiang Xue committed
464 465 466 467 468 469 470
	}

	/**
	 * 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.
	 */
471
	public function attributes()
Qiang Xue committed
472
	{
Qiang Xue committed
473
		return array_keys($this->getTableSchema()->columns);
474 475
	}

w  
Qiang Xue committed
476 477 478 479 480 481 482 483 484 485 486 487 488
	/**
	 * 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)
	{
Qiang Xue committed
489
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
w  
Qiang Xue committed
490 491 492 493 494 495 496 497 498 499 500
	}

	/**
	 * 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.
	 * @see hasAttribute
	 */
	public function setAttribute($name, $value)
	{
Qiang Xue committed
501
		$this->_attributes[$name] = $value;
w  
Qiang Xue committed
502 503
	}

Qiang Xue committed
504 505 506
	public function getChangedAttributes($names = null)
	{
		if ($names === null) {
507
			$names = $this->attributes();
Qiang Xue committed
508 509 510
		}
		$names = array_flip($names);
		$attributes = array();
Qiang Xue committed
511
		if ($this->_oldAttributes === null) {
Qiang Xue committed
512 513 514 515 516 517 518 519 520 521
			foreach ($this->_attributes as $name => $value) {
				if (isset($names[$name])) {
					$attributes[$name] = $value;
				}
			}
		} else {
			foreach ($this->_attributes as $name => $value) {
				if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) {
					$attributes[$name] = $value;
				}
w  
Qiang Xue committed
522
			}
Qiang Xue committed
523
		}
Qiang Xue committed
524
		return $attributes;
w  
Qiang Xue committed
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
	}

	/**
	 * 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)
	{
Qiang Xue committed
552
		if (!$runValidation || $this->validate($attributes)) {
w  
Qiang Xue committed
553
			return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes);
Qiang Xue committed
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
		}
		return false;
	}

	/**
	 * 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 Exception if the record is not new
	 */
	public function insert($attributes = null)
	{
		if ($this->beforeInsert()) {
			$query = new Query;
			$values = $this->getChangedAttributes($attributes);
			$db = $this->getDbConnection();
			$command = $query->insert($this->tableName(), $values)->createCommand($db);
			if ($command->execute()) {
Qiang Xue committed
578
				$table = $this->getTableSchema();
Qiang Xue committed
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
				if ($table->sequenceName !== null) {
					foreach ($table->primaryKey as $name) {
						if (!isset($this->_attributes[$name])) {
							$this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName);
							break;
						}
					}
				}
				foreach ($values as $name => $value) {
					$this->_oldAttributes[$name] = $value;
				}
				$this->afterInsert();
				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 Exception if the record is new
	 */
	public function update($attributes = null)
	{
		if ($this->getIsNewRecord()) {
			throw new Exception('The active record cannot be updated because it is new.');
		}
		if ($this->beforeUpdate()) {
			$values = $this->getChangedAttributes($attributes);
			if ($values !== array()) {
				$this->updateAll($values, $this->getOldPrimaryKey(true));
				foreach ($values as $name => $value) {
					$this->_oldAttributes[$name] = $this->_attributes[$name];
				}
				$this->afterUpdate();
			}
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Saves one or several counter columns for the current AR object.
	 * Note that this method differs from [[updateAllCounters()]] in that it only
	 * saves counters for the current AR object.
	 *
	 * An example usage is as follows:
	 *
	 * ~~~
	 * $post = Post::find($id)->one();
	 * $post->updateCounters(array('view_count' => 1));
	 * ~~~
	 *
	 * Use negative values if you want to decrease the counters.
	 * @param array $counters the counters to be updated (attribute name => increment value)
	 * @return boolean whether the saving is successful
	 * @throws Exception if the record is new or any database error
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
		if ($this->getIsNewRecord()) {
			throw new Exception('The active record cannot be updated because it is new.');
		}
		$this->updateAllCounters($counters, $this->getOldPrimaryKey(true));
		foreach ($counters as $name => $value) {
			$this->_attributes[$name] += $value;
			$this->_oldAttributes[$name] = $this->_attributes[$name];
		}
		return true;
	}

	/**
	 * Deletes the row corresponding to this active record.
	 * @return boolean whether the deletion is successful.
	 * @throws Exception if the record is new or any database error
	 */
	public function delete()
	{
		if ($this->getIsNewRecord()) {
			throw new Exception('The active record cannot be deleted because it is new.');
		}
		if ($this->beforeDelete()) {
			$result = $this->deleteAll($this->getPrimaryKey(true)) > 0;
			$this->_oldAttributes = null;
			$this->afterDelete();
			return $result;
Qiang Xue committed
672
		} else {
w  
Qiang Xue committed
673
			return false;
Qiang Xue committed
674
		}
w  
Qiang Xue committed
675 676 677 678 679 680 681 682 683 684 685
	}

	/**
	 * 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()
	{
Qiang Xue committed
686
		return $this->_oldAttributes === null;
w  
Qiang Xue committed
687 688 689 690 691 692 693 694 695
	}

	/**
	 * 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)
	{
Qiang Xue committed
696
		$this->_oldAttributes = $value ? null : $this->_attributes;
w  
Qiang Xue committed
697 698 699 700
	}

	/**
	 * This method is invoked before saving a record (after validation, if any).
701
	 * The default implementation raises the `beforeSave` event.
w  
Qiang Xue committed
702 703 704 705 706 707
	 * 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.
	 */
Qiang Xue committed
708 709 710
	public function beforeInsert()
	{
		$event = new ModelEvent($this);
711
		$this->trigger('beforeInsert', $event);
Qiang Xue committed
712 713 714 715 716
		return $event->isValid;
	}

	/**
	 * This method is invoked after saving a record successfully.
717
	 * The default implementation raises the `afterSave` event.
Qiang Xue committed
718 719 720 721 722
	 * 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.
	 */
	public function afterInsert()
	{
723
		$this->trigger('afterInsert', new Event($this));
Qiang Xue committed
724 725 726 727
	}

	/**
	 * This method is invoked before saving a record (after validation, if any).
728
	 * The default implementation raises the `beforeSave` event.
Qiang Xue committed
729 730 731 732 733 734 735
	 * 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.
	 */
	public function beforeUpdate()
w  
Qiang Xue committed
736
	{
Qiang Xue committed
737
		$event = new ModelEvent($this);
738
		$this->trigger('beforeUpdate', $event);
Qiang Xue committed
739
		return $event->isValid;
w  
Qiang Xue committed
740 741 742 743
	}

	/**
	 * This method is invoked after saving a record successfully.
744
	 * The default implementation raises the `afterSave` event.
w  
Qiang Xue committed
745 746 747
	 * 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.
	 */
Qiang Xue committed
748
	public function afterUpdate()
w  
Qiang Xue committed
749
	{
750
		$this->trigger('afterUpdate', new Event($this));
w  
Qiang Xue committed
751 752 753 754
	}

	/**
	 * This method is invoked before deleting a record.
755
	 * The default implementation raises the `beforeDelete` event.
w  
Qiang Xue committed
756 757 758 759
	 * 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.
	 */
Qiang Xue committed
760
	public function beforeDelete()
w  
Qiang Xue committed
761
	{
Qiang Xue committed
762
		$event = new ModelEvent($this);
763
		$this->trigger('beforeDelete', $event);
Qiang Xue committed
764
		return $event->isValid;
w  
Qiang Xue committed
765 766 767 768
	}

	/**
	 * This method is invoked after deleting a record.
769
	 * The default implementation raises the `afterDelete` event.
w  
Qiang Xue committed
770 771 772
	 * 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.
	 */
Qiang Xue committed
773
	public function afterDelete()
w  
Qiang Xue committed
774
	{
775
		$this->trigger('afterDelete', new Event($this));
w  
Qiang Xue committed
776 777 778
	}

	/**
Qiang Xue committed
779
	 * Repopulates this active record with the latest data.
Qiang Xue committed
780
	 * @param array $attributes
Qiang Xue committed
781
	 * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record.
w  
Qiang Xue committed
782
	 */
Qiang Xue committed
783
	public function refresh($attributes = null)
w  
Qiang Xue committed
784
	{
Qiang Xue committed
785 786 787 788 789 790 791 792
		if ($this->getIsNewRecord()) {
			return false;
		}
		$record = $this->find()->where($this->getPrimaryKey(true))->one();
		if ($record === null) {
			return false;
		}
		if ($attributes === null) {
793
			foreach ($this->attributes() as $name) {
Qiang Xue committed
794
				$this->_attributes[$name] = $record->_attributes[$name];
Qiang Xue committed
795
			}
Qiang Xue committed
796
			$this->_oldAttributes = $this->_attributes;
Qiang Xue committed
797
		} else {
Qiang Xue committed
798 799 800
			foreach ($attributes as $name) {
				$this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name];
			}
w  
Qiang Xue committed
801
		}
Qiang Xue committed
802
		return true;
w  
Qiang Xue committed
803 804 805
	}

	/**
Qiang Xue committed
806 807 808 809
	 * Compares current active record with another one.
	 * The comparison is made by comparing table name and the primary key values of the two active records.
	 * @param ActiveRecord $record record to compare to
	 * @return boolean whether the two active records refer to the same row in the database table.
w  
Qiang Xue committed
810
	 */
Qiang Xue committed
811
	public function equals($record)
w  
Qiang Xue committed
812
	{
Qiang Xue committed
813
		return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
w  
Qiang Xue committed
814 815 816
	}

	/**
Qiang Xue committed
817
	 * Returns the primary key value.
Qiang Xue committed
818 819
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
	 * the return value will be an array with column name as key and column value as value.
Qiang Xue committed
820 821
	 * @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.
w  
Qiang Xue committed
822
	 */
Qiang Xue committed
823
	public function getPrimaryKey($asArray = false)
w  
Qiang Xue committed
824
	{
Qiang Xue committed
825 826 827
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
Qiang Xue committed
828
		} else {
Qiang Xue committed
829
			$values = array();
Qiang Xue committed
830
			foreach ($keys as $name) {
Qiang Xue committed
831
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
Qiang Xue committed
832 833
			}
			return $values;
w  
Qiang Xue committed
834 835 836 837
		}
	}

	/**
Qiang Xue committed
838 839 840 841
	 * 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.
Qiang Xue committed
842 843
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
	 * the return value will be an array with column name as key and column value as value.
Qiang Xue committed
844 845
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
	 * @return string|array the old primary key value. An array (column name=>column value) is returned if the primary key is composite.
Qiang Xue committed
846
	 * If primary key is not defined, null will be returned.
w  
Qiang Xue committed
847
	 */
Qiang Xue committed
848
	public function getOldPrimaryKey($asArray = false)
w  
Qiang Xue committed
849
	{
Qiang Xue committed
850 851 852
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
Qiang Xue committed
853 854
		} else {
			$values = array();
Qiang Xue committed
855
			foreach ($keys as $name) {
Qiang Xue committed
856 857 858 859
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
w  
Qiang Xue committed
860 861 862 863
	}

	/**
	 * Creates an active record with the given attributes.
Qiang Xue committed
864 865
	 * @param array $row attribute values (name => value)
	 * @return ActiveRecord the newly created active record.
w  
Qiang Xue committed
866
	 */
Qiang Xue committed
867
	public static function create($row)
w  
Qiang Xue committed
868
	{
Qiang Xue committed
869
		$record = static::instantiate($row);
Qiang Xue committed
870
		$columns = static::model()->getTableSchema()->columns;
Qiang Xue committed
871
		foreach ($row as $name => $value) {
Qiang Xue committed
872
			if (isset($columns[$name])) {
Qiang Xue committed
873
				$record->_attributes[$name] = $value;
Qiang Xue committed
874
			} else {
Qiang Xue committed
875
				$record->$name = $value;
w  
Qiang Xue committed
876 877
			}
		}
Qiang Xue committed
878
		$record->_oldAttributes = $record->_attributes;
Qiang Xue committed
879
		return $record;
w  
Qiang Xue committed
880 881 882 883
	}

	/**
	 * Creates an active record instance.
Qiang Xue committed
884
	 * This method is called by [[createRecord()]].
w  
Qiang Xue committed
885 886 887 888
	 * 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.
Alexander Makarov committed
889
	 * @param array $row list of attribute values for the active records.
w  
Qiang Xue committed
890
	 * @return ActiveRecord the active record
w  
Qiang Xue committed
891
	 */
Qiang Xue committed
892
	public static function instantiate($row)
w  
Qiang Xue committed
893
	{
Qiang Xue committed
894
		return new static;
w  
Qiang Xue committed
895 896 897 898 899 900 901 902 903 904 905 906 907
	}

	/**
	 * 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
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}
}