ActiveRecord.php 48.4 KB
Newer Older
w  
Qiang Xue committed
1 2 3 4
<?php
/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
5
 * @copyright Copyright (c) 2008 Yii Software LLC
w  
Qiang Xue committed
6 7 8
 * @license http://www.yiiframework.com/license/
 */

Qiang Xue committed
9
namespace yii\db;
w  
Qiang Xue committed
10

Qiang Xue committed
11
use yii\base\InvalidConfigException;
Qiang Xue committed
12
use yii\base\Model;
Qiang Xue committed
13
use yii\base\InvalidParamException;
Qiang Xue committed
14
use yii\base\ModelEvent;
Qiang Xue committed
15 16
use yii\base\UnknownMethodException;
use yii\base\InvalidCallException;
Qiang Xue committed
17 18 19
use yii\db\Connection;
use yii\db\TableSchema;
use yii\db\Expression;
Qiang Xue committed
20
use yii\helpers\StringHelper;
21
use yii\helpers\Inflector;
w  
Qiang Xue committed
22

w  
Qiang Xue committed
23
/**
Qiang Xue committed
24
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
w  
Qiang Xue committed
25
 *
Qiang Xue committed
26
 * @include @yii/db/ActiveRecord.md
w  
Qiang Xue committed
27
 *
Qiang Xue committed
28
 * @property Connection $db the database connection used by this AR class.
Qiang Xue committed
29 30 31
 * @property TableSchema $tableSchema the schema information of the DB table associated with this AR class.
 * @property array $oldAttributes the old attribute values (name-value pairs).
 * @property array $dirtyAttributes the changed attribute values (name-value pairs).
32
 * @property boolean $isNewRecord whether the record is new and should be inserted when calling [[save()]].
Qiang Xue committed
33 34 35
 * @property mixed $primaryKey the primary key value.
 * @property mixed $oldPrimaryKey the old primary key value.
 *
Qiang Xue committed
36 37
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
w  
Qiang Xue committed
38
 */
Qiang Xue committed
39
class ActiveRecord extends Model
w  
Qiang Xue committed
40
{
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
	/**
	 * @event Event an event that is triggered when the record is initialized via [[init()]].
	 */
	const EVENT_INIT = 'init';
	/**
	 * @event Event an event that is triggered after the record is created and populated with query result.
	 */
	const EVENT_AFTER_FIND = 'afterFind';
	/**
	 * @event ModelEvent an event that is triggered before inserting a record.
	 * You may set [[ModelEvent::isValid]] to be false to stop the insertion.
	 */
	const EVENT_BEFORE_INSERT = 'beforeInsert';
	/**
	 * @event Event an event that is triggered after a record is inserted.
	 */
	const EVENT_AFTER_INSERT = 'afterInsert';
	/**
	 * @event ModelEvent an event that is triggered before updating a record.
	 * You may set [[ModelEvent::isValid]] to be false to stop the update.
	 */
	const EVENT_BEFORE_UPDATE = 'beforeUpdate';
	/**
	 * @event Event an event that is triggered after a record is updated.
	 */
	const EVENT_AFTER_UPDATE = 'afterUpdate';
	/**
	 * @event ModelEvent an event that is triggered before deleting a record.
	 * You may set [[ModelEvent::isValid]] to be false to stop the deletion.
	 */
	const EVENT_BEFORE_DELETE = 'beforeDelete';
	/**
	 * @event Event an event that is triggered after a record is deleted.
	 */
	const EVENT_AFTER_DELETE = 'afterDelete';

77 78 79 80
	/**
	 * Represents insert ActiveRecord operation. This constant is used for specifying set of atomic operations
	 * for particular scenario in the [[scenarios()]] method.
	 */
resurtm committed
81
	const OP_INSERT = 'insert';
82 83 84 85
	/**
	 * Represents update ActiveRecord operation. This constant is used for specifying set of atomic operations
	 * for particular scenario in the [[scenarios()]] method.
	 */
resurtm committed
86
	const OP_UPDATE = 'update';
87 88 89 90
	/**
	 * Represents delete ActiveRecord operation. This constant is used for specifying set of atomic operations
	 * for particular scenario in the [[scenarios()]] method.
	 */
resurtm committed
91
	const OP_DELETE = 'delete';
92

w  
Qiang Xue committed
93
	/**
Qiang Xue committed
94 95 96 97 98
	 * @var array attribute values indexed by attribute names
	 */
	private $_attributes = array();
	/**
	 * @var array old attribute values indexed by attribute names.
w  
Qiang Xue committed
99
	 */
Qiang Xue committed
100
	private $_oldAttributes;
101
	/**
Qiang Xue committed
102
	 * @var array related models indexed by the relation names
103
	 */
Qiang Xue committed
104 105
	private $_related;

106

Qiang Xue committed
107 108 109 110 111 112
	/**
	 * 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.
	 */
Qiang Xue committed
113
	public static function getDb()
Qiang Xue committed
114
	{
Qiang Xue committed
115
		return \Yii::$app->getDb();
Qiang Xue committed
116 117
	}

Qiang Xue committed
118
	/**
Qiang Xue committed
119
	 * Creates an [[ActiveQuery]] instance for query purpose.
Qiang Xue committed
120
	 *
Qiang Xue committed
121
	 * @include @yii/db/ActiveRecord-find.md
Qiang Xue committed
122 123 124
	 *
	 * @param mixed $q the query parameter. This can be one of the followings:
	 *
Qiang Xue committed
125 126
	 *  - a scalar value (integer or string): query by a single primary key value and return the
	 *    corresponding record.
Qiang Xue committed
127
	 *  - an array of name-value pairs: query by a set of column values and return a single record matching all of them.
Qiang Xue committed
128
	 *  - null: return a new [[ActiveQuery]] object for further query purpose.
Qiang Xue committed
129
	 *
Qiang Xue committed
130 131
	 * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance
	 * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be
Qiang Xue committed
132
	 * returned (null will be returned if there is no matching).
Qiang Xue committed
133
	 * @throws InvalidConfigException if the AR class does not have a primary key
Qiang Xue committed
134
	 * @see createQuery()
Qiang Xue committed
135 136 137
	 */
	public static function find($q = null)
	{
Qiang Xue committed
138
		$query = static::createQuery();
Qiang Xue committed
139
		if (is_array($q)) {
Qiang Xue committed
140
			return $query->where($q)->one();
Qiang Xue committed
141 142
		} elseif ($q !== null) {
			// query by primary key
Qiang Xue committed
143
			$primaryKey = static::primaryKey();
Qiang Xue committed
144 145 146 147 148
			if (isset($primaryKey[0])) {
				return $query->where(array($primaryKey[0] => $q))->one();
			} else {
				throw new InvalidConfigException(get_called_class() . ' must have a primary key.');
			}
Qiang Xue committed
149
		}
Qiang Xue committed
150
		return $query;
w  
Qiang Xue committed
151 152
	}

Qiang Xue committed
153
	/**
Qiang Xue committed
154 155 156 157 158 159 160 161 162 163 164 165 166
	 * Creates an [[ActiveQuery]] instance with a given SQL statement.
	 *
	 * Note that because the SQL statement is already specified, calling additional
	 * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]]
	 * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is
	 * still fine.
	 *
	 * Below is an example:
	 *
	 * ~~~
	 * $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all();
	 * ~~~
	 *
Qiang Xue committed
167 168
	 * @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
169
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance
Qiang Xue committed
170
	 */
Qiang Xue committed
171
	public static function findBySql($sql, $params = array())
w  
Qiang Xue committed
172
	{
Qiang Xue committed
173
		$query = static::createQuery();
Qiang Xue committed
174 175 176 177 178 179
		$query->sql = $sql;
		return $query->params($params);
	}

	/**
	 * Updates the whole table using the provided attribute values and conditions.
Qiang Xue committed
180 181 182 183 184 185 186 187
	 * For example, to change the status to be 1 for all customers whose status is 2:
	 *
	 * ~~~
	 * Customer::updateAll(array('status' => 1), 'status = 2');
	 * ~~~
	 *
	 * @param array $attributes attribute values (name-value pairs) to be saved into the table
	 * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
Qiang Xue committed
188
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
189
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
190 191
	 * @return integer the number of rows updated
	 */
Qiang Xue committed
192
	public static function updateAll($attributes, $condition = '', $params = array())
w  
Qiang Xue committed
193
	{
Qiang Xue committed
194
		$command = static::getDb()->createCommand();
Qiang Xue committed
195 196
		$command->update(static::tableName(), $attributes, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
197 198
	}

Qiang Xue committed
199
	/**
Qiang Xue committed
200 201 202 203 204 205 206
	 * Updates the whole table using the provided counter changes and conditions.
	 * For example, to increment all customers' age by 1,
	 *
	 * ~~~
	 * Customer::updateAllCounters(array('age' => 1));
	 * ~~~
	 *
Qiang Xue committed
207
	 * @param array $counters the counters to be updated (attribute name => increment value).
Qiang Xue committed
208 209
	 * Use negative values if you want to decrement the counters.
	 * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
Qiang Xue committed
210
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
211
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
212
	 * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
Qiang Xue committed
213 214 215
	 * @return integer the number of rows updated
	 */
	public static function updateAllCounters($counters, $condition = '', $params = array())
w  
Qiang Xue committed
216
	{
Qiang Xue committed
217
		$n = 0;
Qiang Xue committed
218
		foreach ($counters as $name => $value) {
219
			$counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value));
Qiang Xue committed
220
			$n++;
Qiang Xue committed
221
		}
222
		$command = static::getDb()->createCommand();
Qiang Xue committed
223 224
		$command->update(static::tableName(), $counters, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
225 226
	}

Qiang Xue committed
227 228
	/**
	 * Deletes rows in the table using the provided conditions.
Qiang Xue committed
229 230 231 232 233 234 235 236 237
	 * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
	 *
	 * For example, to delete all customers whose status is 3:
	 *
	 * ~~~
	 * Customer::deleteAll('status = 3');
	 * ~~~
	 *
	 * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
Qiang Xue committed
238
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
239
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
240
	 * @return integer the number of rows deleted
Qiang Xue committed
241
	 */
Qiang Xue committed
242
	public static function deleteAll($condition = '', $params = array())
w  
Qiang Xue committed
243
	{
Qiang Xue committed
244
		$command = static::getDb()->createCommand();
Qiang Xue committed
245 246
		$command->delete(static::tableName(), $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
247 248
	}

.  
Qiang Xue committed
249
	/**
Qiang Xue committed
250 251 252 253
	 * Creates an [[ActiveQuery]] instance.
	 * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query.
	 * You may override this method to return a customized query (e.g. `CustomerQuery` specified
	 * written for querying `Customer` purpose.)
Qiang Xue committed
254
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance.
.  
Qiang Xue committed
255
	 */
Qiang Xue committed
256
	public static function createQuery()
w  
Qiang Xue committed
257
	{
Qiang Xue committed
258 259 260
		return new ActiveQuery(array(
			'modelClass' => get_called_class(),
		));
w  
Qiang Xue committed
261 262 263
	}

	/**
Qiang Xue committed
264
	 * Declares the name of the database table associated with this AR class.
265
	 * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
Qiang Xue committed
266 267
	 * with prefix 'tbl_'. For example, 'Customer' becomes 'tbl_customer', and 'OrderItem' becomes
	 * 'tbl_order_item'. You may override this method if the table is not named after this convention.
w  
Qiang Xue committed
268 269
	 * @return string the table name
	 */
Qiang Xue committed
270
	public static function tableName()
w  
Qiang Xue committed
271
	{
272
		return 'tbl_' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
w  
Qiang Xue committed
273 274 275
	}

	/**
Qiang Xue committed
276 277
	 * 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
278
	 */
Qiang Xue committed
279
	public static function getTableSchema()
w  
Qiang Xue committed
280
	{
Qiang Xue committed
281
		return static::getDb()->getTableSchema(static::tableName());
w  
Qiang Xue committed
282 283 284
	}

	/**
Qiang Xue committed
285 286
	 * Returns the primary key name(s) for this AR class.
	 * The default implementation will return the primary key(s) as declared
Qiang Xue committed
287
	 * in the DB table that is associated with this AR class.
Qiang Xue committed
288
	 *
Qiang Xue committed
289 290 291
	 * 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.
Qiang Xue committed
292 293 294
	 *
	 * Note that an array should be returned even for a table with single primary key.
	 *
Qiang Xue committed
295
	 * @return string[] the primary keys of the associated database table.
w  
Qiang Xue committed
296
	 */
Qiang Xue committed
297
	public static function primaryKey()
w  
Qiang Xue committed
298
	{
Qiang Xue committed
299
		return static::getTableSchema()->primaryKey;
w  
Qiang Xue committed
300 301
	}

302
	/**
303
	 * Returns the name of the column that stores the lock version for implementing optimistic locking.
304
	 *
305 306 307 308
	 * Optimistic locking allows multiple users to access the same record for edits and avoids
	 * potential conflicts. In case when a user attempts to save the record upon some staled data
	 * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
	 * and the update or deletion is skipped.
309 310 311 312 313
	 *
	 * Optimized locking is only supported by [[update()]] and [[delete()]].
	 *
	 * To use optimized locking:
	 *
314
	 * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
315
	 *    Override this method to return the name of this column.
316 317 318 319 320 321 322 323 324
	 * 2. In the Web form that collects the user input, add a hidden field that stores
	 *    the lock version of the recording being updated.
	 * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]]
	 *    and implement necessary business logic (e.g. merging the changes, prompting stated data)
	 *    to resolve the conflict.
	 *
	 * @return string the column name that stores the lock version of a table row.
	 * If null is returned (default implemented), optimistic locking will not be supported.
	 */
325
	public function optimisticLock()
326 327 328 329
	{
		return null;
	}

w  
Qiang Xue committed
330
	/**
Qiang Xue committed
331
	 * PHP getter magic method.
Qiang Xue committed
332
	 * This method is overridden so that attributes and related objects can be accessed like properties.
Qiang Xue committed
333 334 335 336 337 338
	 * @param string $name property name
	 * @return mixed property value
	 * @see getAttribute
	 */
	public function __get($name)
	{
Qiang Xue committed
339
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
Qiang Xue committed
340
			return $this->_attributes[$name];
Qiang Xue committed
341
		} elseif (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
342
			return null;
Qiang Xue committed
343 344 345 346 347 348 349 350
		} else {
			$t = strtolower($name);
			if (isset($this->_related[$t]) || $this->_related !== null && array_key_exists($t, $this->_related)) {
				return $this->_related[$t];
			}
			$value = parent::__get($name);
			if ($value instanceof ActiveRelation) {
				return $this->_related[$t] = $value->multiple ? $value->all() : $value->one();
Qiang Xue committed
351
			} else {
Qiang Xue committed
352
				return $value;
Qiang Xue committed
353
			}
Qiang Xue committed
354 355 356 357 358 359 360 361 362 363 364
		}
	}

	/**
	 * 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
365
		if (isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
366 367 368 369 370 371 372 373
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
Qiang Xue committed
374
	 * This method overrides the parent implementation by checking if the named attribute is null or not.
Qiang Xue committed
375 376 377 378
	 * @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
379
	{
Qiang Xue committed
380 381 382 383
		try {
			return $this->__get($name) !== null;
		} catch (\Exception $e) {
			return false;
Qiang Xue committed
384 385 386 387 388 389 390 391 392 393 394
		}
	}

	/**
	 * 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
395
		if (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
396
			unset($this->_attributes[$name]);
Qiang Xue committed
397
		} else {
Qiang Xue committed
398 399 400 401 402 403
			$t = strtolower($name);
			if (isset($this->_related[$t])) {
				unset($this->_related[$t]);
			} else {
				parent::__unset($name);
			}
Qiang Xue committed
404 405 406
		}
	}

Qiang Xue committed
407 408 409 410
	/**
	 * Declares a `has-one` relation.
	 * The declaration is returned in terms of an [[ActiveRelation]] instance
	 * through which the related record can be queried and retrieved back.
Qiang Xue committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
	 *
	 * A `has-one` relation means that there is at most one related record matching
	 * the criteria set by this relation, e.g., a customer has one country.
	 *
	 * For example, to declare the `country` relation for `Customer` class, we can write
	 * the following code in the `Customer` class:
	 *
	 * ~~~
	 * public function getCountry()
	 * {
	 *     return $this->hasOne('Country', array('id' => 'country_id'));
	 * }
	 * ~~~
	 *
	 * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name
	 * in the related class `Country`, while the 'country_id' value refers to an attribute name
	 * in the current AR class.
	 *
	 * Call methods declared in [[ActiveRelation]] to further customize the relation.
	 *
Qiang Xue committed
431 432 433 434 435 436
	 * @param string $class the class name of the related record
	 * @param array $link the primary-foreign key constraint. The keys of the array refer to
	 * the columns in the table associated with the `$class` model, while the values of the
	 * array refer to the corresponding columns in the table associated with this AR class.
	 * @return ActiveRelation the relation object.
	 */
Qiang Xue committed
437
	public function hasOne($class, $link)
Qiang Xue committed
438
	{
Qiang Xue committed
439 440 441 442 443 444
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => false,
		));
Qiang Xue committed
445 446
	}

Qiang Xue committed
447 448 449 450
	/**
	 * Declares a `has-many` relation.
	 * The declaration is returned in terms of an [[ActiveRelation]] instance
	 * through which the related record can be queried and retrieved back.
Qiang Xue committed
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
	 *
	 * A `has-many` relation means that there are multiple related records matching
	 * the criteria set by this relation, e.g., a customer has many orders.
	 *
	 * For example, to declare the `orders` relation for `Customer` class, we can write
	 * the following code in the `Customer` class:
	 *
	 * ~~~
	 * public function getOrders()
	 * {
	 *     return $this->hasMany('Order', array('customer_id' => 'id'));
	 * }
	 * ~~~
	 *
	 * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
	 * an attribute name in the related class `Order`, while the 'id' value refers to
	 * an attribute name in the current AR class.
	 *
Qiang Xue committed
469 470 471 472 473 474
	 * @param string $class the class name of the related record
	 * @param array $link the primary-foreign key constraint. The keys of the array refer to
	 * the columns in the table associated with the `$class` model, while the values of the
	 * array refer to the corresponding columns in the table associated with this AR class.
	 * @return ActiveRelation the relation object.
	 */
Qiang Xue committed
475
	public function hasMany($class, $link)
Qiang Xue committed
476
	{
Qiang Xue committed
477 478 479 480 481 482
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => true,
		));
Qiang Xue committed
483 484
	}

Qiang Xue committed
485
	/**
Qiang Xue committed
486 487 488 489
	 * Populates the named relation with the related records.
	 * Note that this method does not check if the relation exists or not.
	 * @param string $name the relation name (case-insensitive)
	 * @param ActiveRecord|array|null the related records to be populated into the relation.
Qiang Xue committed
490
	 */
Qiang Xue committed
491
	public function populateRelation($name, $records)
Qiang Xue committed
492
	{
Qiang Xue committed
493
		$this->_related[strtolower($name)] = $records;
Qiang Xue committed
494 495
	}

Qiang Xue committed
496 497
	/**
	 * Returns the list of all attribute names of the model.
Qiang Xue committed
498
	 * The default implementation will return all column names of the table associated with this AR class.
Qiang Xue committed
499 500
	 * @return array list of attribute names.
	 */
501
	public function attributes()
Qiang Xue committed
502
	{
Qiang Xue committed
503
		return array_keys($this->getTableSchema()->columns);
504 505
	}

w  
Qiang Xue committed
506 507 508 509 510 511 512 513 514 515
	/**
	 * Returns the named attribute value.
	 * If this record is the result of a query and the attribute is not loaded,
	 * null will be returned.
	 * @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
516
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
w  
Qiang Xue committed
517 518 519 520 521 522 523 524 525 526
	}

	/**
	 * Sets the named attribute value.
	 * @param string $name the attribute name
	 * @param mixed $value the attribute value.
	 * @see hasAttribute
	 */
	public function setAttribute($name, $value)
	{
Qiang Xue committed
527
		$this->_attributes[$name] = $value;
w  
Qiang Xue committed
528 529
	}

Qiang Xue committed
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
	/**
	 * Returns the old attribute values.
	 * @return array the old attribute values (name-value pairs)
	 */
	public function getOldAttributes()
	{
		return $this->_oldAttributes === null ? array() : $this->_oldAttributes;
	}

	/**
	 * Sets the old attribute values.
	 * All existing old attribute values will be discarded.
	 * @param array $values old attribute values to be set.
	 */
	public function setOldAttributes($values)
	{
		$this->_oldAttributes = $values;
	}

Qiang Xue committed
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
	/**
	 * Returns the old value of the named attribute.
	 * If this record is the result of a query and the attribute is not loaded,
	 * null will be returned.
	 * @param string $name the attribute name
	 * @return mixed the old attribute value. Null if the attribute is not loaded before
	 * or does not exist.
	 * @see hasAttribute
	 */
	public function getOldAttribute($name)
	{
		return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
	}

	/**
	 * Sets the old value of the named attribute.
	 * @param string $name the attribute name
	 * @param mixed $value the old attribute value.
	 * @see hasAttribute
	 */
	public function setOldAttribute($name, $value)
	{
		$this->_oldAttributes[$name] = $value;
	}

	/**
	 * Returns a value indicating whether the named attribute has been changed.
	 * @param string $name the name of the attribute
	 * @return boolean whether the attribute has been changed
	 */
	public function isAttributeChanged($name)
	{
581 582
		if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
			return $this->_attributes[$name] !== $this->_oldAttributes[$name];
Qiang Xue committed
583 584 585 586 587
		} else {
			return isset($this->_attributes[$name]) || isset($this->_oldAttributes);
		}
	}

Qiang Xue committed
588 589 590 591 592 593
	/**
	 * Returns the attribute values that have been modified since they are loaded or saved most recently.
	 * @param string[]|null $names the names of the attributes whose values may be returned if they are
	 * changed recently. If null, [[attributes()]] will be used.
	 * @return array the changed attribute values (name-value pairs)
	 */
Qiang Xue committed
594
	public function getDirtyAttributes($names = null)
Qiang Xue committed
595 596
	{
		if ($names === null) {
597
			$names = $this->attributes();
Qiang Xue committed
598 599 600
		}
		$names = array_flip($names);
		$attributes = array();
Qiang Xue committed
601
		if ($this->_oldAttributes === null) {
Qiang Xue committed
602 603 604 605 606 607 608 609 610 611
			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
612
			}
Qiang Xue committed
613
		}
Qiang Xue committed
614
		return $attributes;
w  
Qiang Xue committed
615 616 617 618 619
	}

	/**
	 * Saves the current record.
	 *
Qiang Xue committed
620 621 622 623
	 * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]]
	 * when [[isNewRecord]] is false.
	 *
	 * For example, to save a customer record:
w  
Qiang Xue committed
624
	 *
Qiang Xue committed
625 626 627 628 629 630
	 * ~~~
	 * $customer = new Customer;  // or $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->save();
	 * ~~~
w  
Qiang Xue committed
631 632 633 634 635 636 637 638 639 640
	 *
	 *
	 * @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)
	{
641 642 643 644 645
		if ($this->getIsNewRecord()) {
			return $this->insert($runValidation, $attributes);
		} else {
			return $this->update($runValidation, $attributes) !== false;
		}
Qiang Xue committed
646 647 648
	}

	/**
Qiang Xue committed
649 650 651 652 653 654
	 * Inserts a row into the associated database table using the attribute values of this record.
	 *
	 * This method performs the following steps in order:
	 *
	 * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
	 *    fails, it will skip the rest of the steps;
655 656
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
657
	 *    rest of the steps;
658 659
	 * 4. insert the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
660
	 *
661
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
662 663
	 * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
664 665 666 667
	 *
	 * Only the [[changedAttributes|changed attribute values]] will be inserted into database.
	 *
	 * If the table's primary key is auto-incremental and is null during insertion,
Qiang Xue committed
668
	 * it will be populated with the actual value after insertion.
Qiang Xue committed
669 670 671 672 673 674 675 676 677 678 679 680
	 *
	 * For example, to insert a customer record:
	 *
	 * ~~~
	 * $customer = new Customer;
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->insert();
	 * ~~~
	 *
	 * @param boolean $runValidation whether to perform validation before saving the record.
	 * If the validation fails, the record will not be inserted into the database.
Qiang Xue committed
681 682 683
	 * @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.
684
	 * @throws \Exception in case insert failed.
Qiang Xue committed
685
	 */
Qiang Xue committed
686
	public function insert($runValidation = true, $attributes = null)
Qiang Xue committed
687
	{
688 689 690 691
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
		$db = static::getDb();
692
		$transaction = $this->isOperationAtomic(self::OP_INSERT) && $db->getTransaction() === null ? $db->beginTransaction() : null;
693
		try {
resurtm committed
694 695 696
			$result = $this->insertInternal($attributes);
			if ($transaction !== null) {
				if ($result === false) {
697 698 699 700 701 702
					$transaction->rollback();
				} else {
					$transaction->commit();
				}
			}
		} catch (\Exception $e) {
resurtm committed
703
			if ($transaction !== null) {
704 705 706 707 708 709 710 711 712 713
				$transaction->rollback();
			}
			throw $e;
		}
		return $result;
	}

	/**
	 * @see ActiveRecord::insert()
	 */
resurtm committed
714
	private function insertInternal($attributes = null)
715 716
	{
		if (!$this->beforeSave(true)) {
Qiang Xue committed
717 718
			return false;
		}
719 720 721 722
		$values = $this->getDirtyAttributes($attributes);
		if (empty($values)) {
			foreach ($this->primaryKey() as $key) {
				$values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null;
Qiang Xue committed
723
			}
724 725 726
		}
		$db = static::getDb();
		$command = $db->createCommand()->insert($this->tableName(), $values);
727 728 729 730 731 732 733 734 735
		if (!$command->execute()) {
			return false;
		}
		$table = $this->getTableSchema();
		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;
Qiang Xue committed
736 737 738
				}
			}
		}
739 740 741 742 743
		foreach ($values as $name => $value) {
			$this->_oldAttributes[$name] = $value;
		}
		$this->afterSave(true);
		return true;
Qiang Xue committed
744 745 746
	}

	/**
Qiang Xue committed
747 748 749 750 751 752
	 * Saves the changes to this active record into the associated database table.
	 *
	 * This method performs the following steps in order:
	 *
	 * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
	 *    fails, it will skip the rest of the steps;
753 754
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
755
	 *    rest of the steps;
756 757
	 * 4. save the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
758
	 *
759
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
760 761
	 * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
762 763 764 765 766 767 768 769 770 771 772 773
	 *
	 * Only the [[changedAttributes|changed attribute values]] will be saved into database.
	 *
	 * For example, to update a customer record:
	 *
	 * ~~~
	 * $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->update();
	 * ~~~
	 *
774 775 776 777 778 779 780 781 782 783 784 785
	 * Note that it is possible the update does not affect any row in the table.
	 * In this case, this method will return 0. For this reason, you should use the following
	 * code to check if update() is successful or not:
	 *
	 * ~~~
	 * if ($this->update() !== false) {
	 *     // update successful
	 * } else {
	 *     // update failed
	 * }
	 * ~~~
	 *
Qiang Xue committed
786 787
	 * @param boolean $runValidation whether to perform validation before saving the record.
	 * If the validation fails, the record will not be inserted into the database.
Qiang Xue committed
788 789
	 * @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.
790 791
	 * @return integer|boolean the number of rows affected, or false if validation fails
	 * or [[beforeSave()]] stops the updating process.
792
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
793
	 * being updated is outdated.
794
	 * @throws \Exception in case update failed.
Qiang Xue committed
795
	 */
Qiang Xue committed
796
	public function update($runValidation = true, $attributes = null)
Qiang Xue committed
797
	{
798
		if ($runValidation && !$this->validate($attributes)) {
Qiang Xue committed
799 800
			return false;
		}
801
		$db = static::getDb();
802
		$transaction = $this->isOperationAtomic(self::OP_UPDATE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
803
		try {
resurtm committed
804 805 806
			$result = $this->updateInternal($attributes);
			if ($transaction !== null) {
				if ($result === false) {
807 808 809
					$transaction->rollback();
				} else {
					$transaction->commit();
810
				}
811
			}
812
		} catch (\Exception $e) {
resurtm committed
813
			if ($transaction !== null) {
814
				$transaction->rollback();
815
			}
816 817 818 819
			throw $e;
		}
		return $result;
	}
820

821 822 823 824
	/**
	 * @see CActiveRecord::update()
	 * @throws StaleObjectException
	 */
resurtm committed
825
	private function updateInternal($attributes = null)
826 827 828 829 830 831 832 833 834 835 836 837 838
	{
		if (!$this->beforeSave(false)) {
			return false;
		}
		$values = $this->getDirtyAttributes($attributes);
		if (empty($values)) {
			return 0;
		}
		$condition = $this->getOldPrimaryKey(true);
		$lock = $this->optimisticLock();
		if ($lock !== null) {
			if (!isset($values[$lock])) {
				$values[$lock] = $this->$lock + 1;
Qiang Xue committed
839
			}
840 841 842 843 844
			$condition[$lock] = $this->$lock;
		}
		// We do not check the return value of updateAll() because it's possible
		// that the UPDATE statement doesn't change anything and thus returns 0.
		$rows = $this->updateAll($values, $condition);
845

846 847 848 849 850 851
		if ($lock !== null && !$rows) {
			throw new StaleObjectException('The object being updated is outdated.');
		}

		foreach ($values as $name => $value) {
			$this->_oldAttributes[$name] = $this->_attributes[$name];
Qiang Xue committed
852
		}
853 854
		$this->afterSave(false);
		return $rows;
Qiang Xue committed
855 856 857
	}

	/**
Qiang Xue committed
858
	 * Updates one or several counter columns for the current AR object.
Qiang Xue committed
859 860 861 862 863 864
	 * Note that this method differs from [[updateAllCounters()]] in that it only
	 * saves counters for the current AR object.
	 *
	 * An example usage is as follows:
	 *
	 * ~~~
Qiang Xue committed
865
	 * $post = Post::find($id);
Qiang Xue committed
866 867 868 869
	 * $post->updateCounters(array('view_count' => 1));
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value)
Qiang Xue committed
870
	 * Use negative values if you want to decrement the counters.
Qiang Xue committed
871 872 873 874 875
	 * @return boolean whether the saving is successful
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
Qiang Xue committed
876 877 878 879 880 881 882 883
		if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
			foreach ($counters as $name => $value) {
				$this->_attributes[$name] += $value;
				$this->_oldAttributes[$name] = $this->_attributes[$name];
			}
			return true;
		} else {
			return false;
Qiang Xue committed
884 885 886 887
		}
	}

	/**
Qiang Xue committed
888 889 890 891 892 893 894 895 896
	 * Deletes the table row corresponding to this active record.
	 *
	 * This method performs the following steps in order:
	 *
	 * 1. call [[beforeDelete()]]. If the method returns false, it will skip the
	 *    rest of the steps;
	 * 2. delete the record from the database;
	 * 3. call [[afterDelete()]].
	 *
Qiang Xue committed
897
	 * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
Qiang Xue committed
898 899
	 * will be raised by the corresponding methods.
	 *
900 901
	 * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason.
	 * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
902
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
903
	 * being deleted is outdated.
904
	 * @throws \Exception in case delete failed.
Qiang Xue committed
905 906 907
	 */
	public function delete()
	{
908
		$db = static::getDb();
909
		$transaction = $this->isOperationAtomic(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
910 911 912 913 914 915 916
		try {
			$result = false;
			if ($this->beforeDelete()) {
				// we do not check the return value of deleteAll() because it's possible
				// the record is already deleted in the database and thus the method will return 0
				$condition = $this->getOldPrimaryKey(true);
				$lock = $this->optimisticLock();
resurtm committed
917
				if ($lock !== null) {
918 919 920
					$condition[$lock] = $this->$lock;
				}
				$result = $this->deleteAll($condition);
resurtm committed
921
				if ($lock !== null && !$result) {
922 923 924 925
					throw new StaleObjectException('The object being deleted is outdated.');
				}
				$this->_oldAttributes = null;
				$this->afterDelete();
926
			}
resurtm committed
927 928
			if ($transaction !== null) {
				if ($result === false) {
929 930 931 932
					$transaction->rollback();
				} else {
					$transaction->commit();
				}
933
			}
934
		} catch (\Exception $e) {
resurtm committed
935
			if ($transaction !== null) {
936 937 938
				$transaction->rollback();
			}
			throw $e;
Qiang Xue committed
939
		}
940
		return $result;
w  
Qiang Xue committed
941 942 943
	}

	/**
Qiang Xue committed
944
	 * Returns a value indicating whether the current record is new.
Qiang Xue committed
945
	 * @return boolean whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
946 947 948
	 */
	public function getIsNewRecord()
	{
Qiang Xue committed
949
		return $this->_oldAttributes === null;
w  
Qiang Xue committed
950 951
	}

952 953 954
	/**
	 * Initializes the object.
	 * This method is called at the end of the constructor.
Qiang Xue committed
955
	 * The default implementation will trigger an [[EVENT_INIT]] event.
956 957 958 959 960 961
	 * If you override this method, make sure you call the parent implementation at the end
	 * to ensure triggering of the event.
	 */
	public function init()
	{
		parent::init();
962
		$this->trigger(self::EVENT_INIT);
963 964 965 966
	}

	/**
	 * This method is called when the AR object is created and populated with the query result.
Qiang Xue committed
967
	 * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
968 969 970 971 972
	 * When overriding this method, make sure you call the parent implementation to ensure the
	 * event is triggered.
	 */
	public function afterFind()
	{
973
		$this->trigger(self::EVENT_AFTER_FIND);
974 975
	}

w  
Qiang Xue committed
976
	/**
Qiang Xue committed
977
	 * Sets the value indicating whether the record is new.
Qiang Xue committed
978
	 * @param boolean $value whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
979 980 981 982
	 * @see getIsNewRecord
	 */
	public function setIsNewRecord($value)
	{
Qiang Xue committed
983
		$this->_oldAttributes = $value ? null : $this->_attributes;
w  
Qiang Xue committed
984 985
	}

Qiang Xue committed
986 987
	/**
	 * This method is called at the beginning of inserting or updating a record.
Qiang Xue committed
988 989
	 * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true,
	 * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false.
Qiang Xue committed
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
	 * When overriding this method, make sure you call the parent implementation like the following:
	 *
	 * ~~~
	 * public function beforeSave($insert)
	 * {
	 *     if (parent::beforeSave($insert)) {
	 *         // ...custom code here...
	 *         return true;
	 *     } else {
	 *         return false;
	 *     }
	 * }
	 * ~~~
	 *
	 * @param boolean $insert whether this method called while inserting a record.
	 * If false, it means the method is called while updating a record.
	 * @return boolean whether the insertion or updating should continue.
	 * If false, the insertion or updating will be cancelled.
	 */
Qiang Xue committed
1009
	public function beforeSave($insert)
w  
Qiang Xue committed
1010
	{
Qiang Xue committed
1011
		$event = new ModelEvent;
1012
		$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
Qiang Xue committed
1013
		return $event->isValid;
w  
Qiang Xue committed
1014 1015
	}

Qiang Xue committed
1016 1017
	/**
	 * This method is called at the end of inserting or updating a record.
Qiang Xue committed
1018 1019
	 * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true,
	 * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false.
Qiang Xue committed
1020 1021 1022 1023 1024
	 * When overriding this method, make sure you call the parent implementation so that
	 * the event is triggered.
	 * @param boolean $insert whether this method called while inserting a record.
	 * If false, it means the method is called while updating a record.
	 */
Qiang Xue committed
1025
	public function afterSave($insert)
w  
Qiang Xue committed
1026
	{
1027
		$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE);
w  
Qiang Xue committed
1028 1029 1030 1031
	}

	/**
	 * This method is invoked before deleting a record.
Qiang Xue committed
1032
	 * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
Qiang Xue committed
1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
	 * When overriding this method, make sure you call the parent implementation like the following:
	 *
	 * ~~~
	 * public function beforeDelete()
	 * {
	 *     if (parent::beforeDelete()) {
	 *         // ...custom code here...
	 *         return true;
	 *     } else {
	 *         return false;
	 *     }
	 * }
	 * ~~~
	 *
w  
Qiang Xue committed
1047 1048
	 * @return boolean whether the record should be deleted. Defaults to true.
	 */
Qiang Xue committed
1049
	public function beforeDelete()
w  
Qiang Xue committed
1050
	{
Qiang Xue committed
1051
		$event = new ModelEvent;
1052
		$this->trigger(self::EVENT_BEFORE_DELETE, $event);
Qiang Xue committed
1053
		return $event->isValid;
w  
Qiang Xue committed
1054 1055 1056 1057
	}

	/**
	 * This method is invoked after deleting a record.
Qiang Xue committed
1058
	 * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
w  
Qiang Xue committed
1059 1060 1061
	 * 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
1062
	public function afterDelete()
w  
Qiang Xue committed
1063
	{
1064
		$this->trigger(self::EVENT_AFTER_DELETE);
w  
Qiang Xue committed
1065 1066 1067
	}

	/**
Qiang Xue committed
1068
	 * Repopulates this active record with the latest data.
Qiang Xue committed
1069
	 * @param array $attributes
Qiang Xue committed
1070 1071
	 * @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
1072
	 */
Qiang Xue committed
1073
	public function refresh($attributes = null)
w  
Qiang Xue committed
1074
	{
Qiang Xue committed
1075
		$record = $this->find($this->getPrimaryKey(true));
Qiang Xue committed
1076 1077 1078 1079
		if ($record === null) {
			return false;
		}
		if ($attributes === null) {
1080
			foreach ($this->attributes() as $name) {
Qiang Xue committed
1081
				$this->_attributes[$name] = $record->_attributes[$name];
Qiang Xue committed
1082
			}
Qiang Xue committed
1083
			$this->_oldAttributes = $this->_attributes;
Qiang Xue committed
1084
		} else {
Qiang Xue committed
1085 1086 1087
			foreach ($attributes as $name) {
				$this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name];
			}
w  
Qiang Xue committed
1088
		}
Qiang Xue committed
1089
		return true;
w  
Qiang Xue committed
1090 1091 1092
	}

	/**
Qiang Xue committed
1093 1094
	 * Returns a value indicating whether the given active record is the same as the current one.
	 * The comparison is made by comparing the table names and the primary key values of the two active records.
Qiang Xue committed
1095
	 * @param ActiveRecord $record record to compare to
Qiang Xue committed
1096
	 * @return boolean whether the two active records refer to the same row in the same database table.
w  
Qiang Xue committed
1097
	 */
Qiang Xue committed
1098
	public function equals($record)
w  
Qiang Xue committed
1099
	{
Qiang Xue committed
1100
		return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
w  
Qiang Xue committed
1101 1102 1103
	}

	/**
Qiang Xue committed
1104
	 * Returns the primary key value(s).
Qiang Xue committed
1105
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
Qiang Xue committed
1106
	 * the return value will be an array with column names as keys and column values as values.
1107
	 * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
resurtm committed
1108
	 * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1109 1110
	 * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
	 * the key value is null).
w  
Qiang Xue committed
1111
	 */
Qiang Xue committed
1112
	public function getPrimaryKey($asArray = false)
w  
Qiang Xue committed
1113
	{
Qiang Xue committed
1114 1115 1116
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
Qiang Xue committed
1117
		} else {
Qiang Xue committed
1118
			$values = array();
Qiang Xue committed
1119
			foreach ($keys as $name) {
Qiang Xue committed
1120
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
Qiang Xue committed
1121 1122
			}
			return $values;
w  
Qiang Xue committed
1123 1124 1125 1126
		}
	}

	/**
Qiang Xue committed
1127
	 * Returns the old primary key value(s).
Qiang Xue committed
1128 1129 1130
	 * 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
1131 1132
	 * @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
1133
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
resurtm committed
1134
	 * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1135 1136
	 * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
	 * the key value is null).
w  
Qiang Xue committed
1137
	 */
Qiang Xue committed
1138
	public function getOldPrimaryKey($asArray = false)
w  
Qiang Xue committed
1139
	{
Qiang Xue committed
1140 1141 1142
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
Qiang Xue committed
1143 1144
		} else {
			$values = array();
Qiang Xue committed
1145
			foreach ($keys as $name) {
Qiang Xue committed
1146 1147 1148 1149
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
w  
Qiang Xue committed
1150 1151 1152
	}

	/**
Qiang Xue committed
1153
	 * Creates an active record object using a row of data.
Qiang Xue committed
1154 1155
	 * This method is called by [[ActiveQuery]] to populate the query results
	 * into Active Records.
Qiang Xue committed
1156 1157
	 * @param array $row attribute values (name => value)
	 * @return ActiveRecord the newly created active record.
w  
Qiang Xue committed
1158
	 */
Qiang Xue committed
1159
	public static function create($row)
w  
Qiang Xue committed
1160
	{
Qiang Xue committed
1161
		$record = static::instantiate($row);
1162
		$columns = static::getTableSchema()->columns;
Qiang Xue committed
1163
		foreach ($row as $name => $value) {
Qiang Xue committed
1164
			if (isset($columns[$name])) {
Qiang Xue committed
1165
				$record->_attributes[$name] = $value;
Qiang Xue committed
1166
			} else {
Qiang Xue committed
1167
				$record->$name = $value;
w  
Qiang Xue committed
1168 1169
			}
		}
Qiang Xue committed
1170
		$record->_oldAttributes = $record->_attributes;
1171
		$record->afterFind();
Qiang Xue committed
1172
		return $record;
w  
Qiang Xue committed
1173 1174 1175 1176
	}

	/**
	 * Creates an active record instance.
Qiang Xue committed
1177
	 * This method is called by [[create()]].
w  
Qiang Xue committed
1178
	 * You may override this method if the instance being created
Qiang Xue committed
1179
	 * depends on the row data to be populated into the record.
w  
Qiang Xue committed
1180 1181
	 * For example, by creating a record based on the value of a column,
	 * you may implement the so-called single-table inheritance mapping.
Qiang Xue committed
1182 1183
	 * @param array $row row data to be populated into the record.
	 * @return ActiveRecord the newly created active record
w  
Qiang Xue committed
1184
	 */
Qiang Xue committed
1185
	public static function instantiate($row)
w  
Qiang Xue committed
1186
	{
Qiang Xue committed
1187
		return new static;
w  
Qiang Xue committed
1188 1189 1190 1191 1192 1193
	}

	/**
	 * 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
Qiang Xue committed
1194
	 * @return boolean whether there is an element at the specified offset.
w  
Qiang Xue committed
1195 1196 1197 1198 1199
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}
Qiang Xue committed
1200

Qiang Xue committed
1201
	/**
Qiang Xue committed
1202 1203 1204 1205 1206
	 * Returns the relation object with the specified name.
	 * A relation is defined by a getter method which returns an [[ActiveRelation]] object.
	 * It can be declared in either the Active Record class itself or one of its behaviors.
	 * @param string $name the relation name
	 * @return ActiveRelation the relation object
Qiang Xue committed
1207
	 * @throws InvalidParamException if the named relation does not exist.
Qiang Xue committed
1208 1209 1210 1211 1212 1213 1214 1215 1216
	 */
	public function getRelation($name)
	{
		$getter = 'get' . $name;
		try {
			$relation = $this->$getter();
			if ($relation instanceof ActiveRelation) {
				return $relation;
			}
Qiang Xue committed
1217
		} catch (UnknownMethodException $e) {
1218
			throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
Qiang Xue committed
1219 1220 1221
		}
	}

Qiang Xue committed
1222
	/**
Qiang Xue committed
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238
	 * Establishes the relationship between two models.
	 *
	 * The relationship is established by setting the foreign key value(s) in one model
	 * to be the corresponding primary key value(s) in the other model.
	 * The model with the foreign key will be saved into database without performing validation.
	 *
	 * If the relationship involves a pivot table, a new row will be inserted into the
	 * pivot table which contains the primary key values from both models.
	 *
	 * Note that this method requires that the primary key value is not null.
	 *
	 * @param string $name the name of the relationship
	 * @param ActiveRecord $model the model to be linked with the current one.
	 * @param array $extraColumns additional column values to be saved into the pivot table.
	 * This parameter is only meaningful for a relationship involving a pivot table
	 * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.)
Qiang Xue committed
1239
	 * @throws InvalidCallException if the method is unable to link two models.
Qiang Xue committed
1240
	 */
Qiang Xue committed
1241
	public function link($name, $model, $extraColumns = array())
Qiang Xue committed
1242
	{
1243 1244 1245 1246
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1247 1248
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1249
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1250
				$viaClass = $viaRelation->modelClass;
1251
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1252
				// unset $viaName so that it can be reloaded to reflect the change
Qiang Xue committed
1253
				unset($this->_related[strtolower($viaName)]);
1254
			} else {
Qiang Xue committed
1255
				$viaRelation = $relation->via;
1256 1257 1258
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1259
			foreach ($viaRelation->link as $a => $b) {
1260 1261 1262 1263 1264
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1265
			foreach ($extraColumns as $k => $v) {
1266 1267
				$columns[$k] = $v;
			}
Qiang Xue committed
1268
			static::getDb()->createCommand()
Qiang Xue committed
1269 1270 1271 1272 1273 1274
				->insert($viaTable, $columns)->execute();
		} else {
			$p1 = $model->isPrimaryKey(array_keys($relation->link));
			$p2 = $this->isPrimaryKey(array_values($relation->link));
			if ($p1 && $p2) {
				if ($this->getIsNewRecord() && $model->getIsNewRecord()) {
Qiang Xue committed
1275
					throw new InvalidCallException('Unable to link models: both models are newly created.');
Qiang Xue committed
1276 1277
				} elseif ($this->getIsNewRecord()) {
					$this->bindModels(array_flip($relation->link), $this, $model);
Qiang Xue committed
1278
				} else {
Qiang Xue committed
1279
					$this->bindModels($relation->link, $model, $this);
1280
				}
Qiang Xue committed
1281 1282 1283 1284
			} elseif ($p1) {
				$this->bindModels(array_flip($relation->link), $this, $model);
			} elseif ($p2) {
				$this->bindModels($relation->link, $model, $this);
1285
			} else {
Qiang Xue committed
1286
				throw new InvalidCallException('Unable to link models: the link does not involve any primary key.');
1287 1288
			}
		}
Qiang Xue committed
1289

Qiang Xue committed
1290
		// update lazily loaded related objects
Qiang Xue committed
1291 1292 1293 1294 1295 1296 1297 1298 1299 1300
		if (!$relation->multiple) {
			$this->_related[$name] = $model;
		} elseif (isset($this->_related[$name])) {
			if ($relation->indexBy !== null) {
				$indexBy = $relation->indexBy;
				$this->_related[$name][$model->$indexBy] = $model;
			} else {
				$this->_related[$name][] = $model;
			}
		}
1301 1302 1303
	}

	/**
Qiang Xue committed
1304 1305 1306 1307 1308 1309 1310
	 * Destroys the relationship between two models.
	 *
	 * The model with the foreign key of the relationship will be deleted if `$delete` is true.
	 * Otherwise, the foreign key will be set null and the model will be saved without validation.
	 *
	 * @param string $name the name of the relationship.
	 * @param ActiveRecord $model the model to be unlinked from the current one.
Qiang Xue committed
1311 1312
	 * @param boolean $delete whether to delete the model that contains the foreign key.
	 * If false, the model's foreign key will be set null and saved.
Qiang Xue committed
1313
	 * If true, the model containing the foreign key will be deleted.
Qiang Xue committed
1314
	 * @throws InvalidCallException if the models cannot be unlinked
1315
	 */
Qiang Xue committed
1316
	public function unlink($name, $model, $delete = false)
1317 1318 1319 1320 1321
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1322 1323
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1324
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1325
				$viaClass = $viaRelation->modelClass;
1326
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1327
				unset($this->_related[strtolower($viaName)]);
1328
			} else {
Qiang Xue committed
1329
				$viaRelation = $relation->via;
1330 1331 1332
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1333
			foreach ($viaRelation->link as $a => $b) {
1334 1335 1336 1337 1338
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1339
			$command = static::getDb()->createCommand();
Qiang Xue committed
1340 1341 1342 1343 1344 1345 1346 1347
			if ($delete) {
				$command->delete($viaTable, $columns)->execute();
			} else {
				$nulls = array();
				foreach (array_keys($columns) as $a) {
					$nulls[$a] = null;
				}
				$command->update($viaTable, $nulls, $columns)->execute();
1348 1349
			}
		} else {
Qiang Xue committed
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362
			$p1 = $model->isPrimaryKey(array_keys($relation->link));
			$p2 = $this->isPrimaryKey(array_values($relation->link));
			if ($p1 && $p2 || $p2) {
				foreach ($relation->link as $a => $b) {
					$model->$a = null;
				}
				$delete ? $model->delete() : $model->save(false);
			} elseif ($p1) {
				foreach ($relation->link as $b) {
					$this->$b = null;
				}
				$delete ? $this->delete() : $this->save(false);
			} else {
Qiang Xue committed
1363
				throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
Qiang Xue committed
1364
			}
1365
		}
Qiang Xue committed
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376

		if (!$relation->multiple) {
			unset($this->_related[$name]);
		} elseif (isset($this->_related[$name])) {
			/** @var $b ActiveRecord */
			foreach ($this->_related[$name] as $a => $b) {
				if ($model->getPrimaryKey() == $b->getPrimaryKey()) {
					unset($this->_related[$name][$a]);
				}
			}
		}
1377 1378
	}

Qiang Xue committed
1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
	/**
	 * Changes the given class name into a namespaced one.
	 * If the given class name is already namespaced, no change will be made.
	 * Otherwise, the class name will be changed to use the same namespace as
	 * the current AR class.
	 * @param string $class the class name to be namespaced
	 * @return string the namespaced class name
	 */
	protected function getNamespacedClass($class)
	{
		if (strpos($class, '\\') === false) {
			$primaryClass = get_class($this);
			if (($pos = strrpos($primaryClass, '\\')) !== false) {
				return substr($primaryClass, 0, $pos + 1) . $class;
			}
		}
		return $class;
	}

1398
	/**
Qiang Xue committed
1399 1400 1401
	 * @param array $link
	 * @param ActiveRecord $foreignModel
	 * @param ActiveRecord $primaryModel
Qiang Xue committed
1402
	 * @throws InvalidCallException
1403
	 */
Qiang Xue committed
1404
	private function bindModels($link, $foreignModel, $primaryModel)
1405
	{
Qiang Xue committed
1406 1407 1408
		foreach ($link as $fk => $pk) {
			$value = $primaryModel->$pk;
			if ($value === null) {
Qiang Xue committed
1409
				throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
Qiang Xue committed
1410
			}
Qiang Xue committed
1411
			$foreignModel->$fk = $value;
Qiang Xue committed
1412
		}
Qiang Xue committed
1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428
		$foreignModel->save(false);
	}

	/**
	 * @param array $keys
	 * @return boolean
	 */
	private function isPrimaryKey($keys)
	{
		$pks = $this->primaryKey();
		foreach ($keys as $key) {
			if (!in_array($key, $pks, true)) {
				return false;
			}
		}
		return true;
Qiang Xue committed
1429
	}
1430 1431

	/**
1432
	 * @param string $operation possible values are ActiveRecord::INSERT, ActiveRecord::UPDATE and ActiveRecord::DELETE.
1433 1434
	 * @return boolean whether given operation is atomic. Currently active scenario is taken into account.
	 */
1435
	private function isOperationAtomic($operation)
1436 1437 1438
	{
		$scenario = $this->getScenario();
		$scenarios = $this->scenarios();
resurtm committed
1439
		if (isset($scenarios[$scenario], $scenario[$scenario]['atomic']) && is_array($scenarios[$scenario]['atomic'])) {
1440
			return in_array($operation, $scenarios[$scenario]['atomic']);
resurtm committed
1441
		} else {
1442 1443 1444
			return false;
		}
	}
w  
Qiang Xue committed
1445
}