ActiveRecord.php 42.6 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\Model;
Qiang Xue committed
12
use yii\base\InvalidParamException;
Qiang Xue committed
13
use yii\base\ModelEvent;
Qiang Xue committed
14 15
use yii\base\UnknownMethodException;
use yii\base\InvalidCallException;
Qiang Xue committed
16 17 18
use yii\db\Connection;
use yii\db\TableSchema;
use yii\db\Expression;
Qiang Xue committed
19
use yii\helpers\StringHelper;
w  
Qiang Xue committed
20

w  
Qiang Xue committed
21
/**
Qiang Xue committed
22
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
w  
Qiang Xue committed
23
 *
Qiang Xue committed
24
 * @include @yii/db/ActiveRecord.md
w  
Qiang Xue committed
25
 *
Qiang Xue committed
26
 * @property Connection $db the database connection used by this AR class.
Qiang Xue committed
27 28 29 30 31 32 33
 * @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).
 * @property boolean $isPrimaryKey whether the record is new and should be inserted when calling [[save()]].
 * @property mixed $primaryKey the primary key value.
 * @property mixed $oldPrimaryKey the old primary key value.
 *
Qiang Xue committed
34 35
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
w  
Qiang Xue committed
36
 */
Qiang Xue committed
37
class ActiveRecord extends Model
w  
Qiang Xue committed
38
{
39 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
	/**
	 * @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';

w  
Qiang Xue committed
75
	/**
Qiang Xue committed
76 77 78 79 80
	 * @var array attribute values indexed by attribute names
	 */
	private $_attributes = array();
	/**
	 * @var array old attribute values indexed by attribute names.
w  
Qiang Xue committed
81
	 */
Qiang Xue committed
82
	private $_oldAttributes;
83
	/**
Qiang Xue committed
84
	 * @var array related models indexed by the relation names
85
	 */
Qiang Xue committed
86 87
	private $_related;

88

Qiang Xue committed
89 90 91 92 93 94
	/**
	 * 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
95
	public static function getDb()
Qiang Xue committed
96
	{
Qiang Xue committed
97
		return \Yii::$app->getDb();
Qiang Xue committed
98 99
	}

Qiang Xue committed
100
	/**
Qiang Xue committed
101
	 * Creates an [[ActiveQuery]] instance for query purpose.
Qiang Xue committed
102
	 *
Qiang Xue committed
103
	 * @include @yii/db/ActiveRecord-find.md
Qiang Xue committed
104 105 106
	 *
	 * @param mixed $q the query parameter. This can be one of the followings:
	 *
Qiang Xue committed
107 108
	 *  - a scalar value (integer or string): query by a single primary key value and return the
	 *    corresponding record.
Qiang Xue committed
109
	 *  - 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
110
	 *  - null: return a new [[ActiveQuery]] object for further query purpose.
Qiang Xue committed
111
	 *
Qiang Xue committed
112 113
	 * @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
114 115
	 * returned (null will be returned if there is no matching).
	 * @see createQuery()
Qiang Xue committed
116 117 118
	 */
	public static function find($q = null)
	{
Qiang Xue committed
119
		$query = static::createQuery();
Qiang Xue committed
120
		if (is_array($q)) {
Qiang Xue committed
121
			return $query->where($q)->one();
Qiang Xue committed
122 123
		} elseif ($q !== null) {
			// query by primary key
Qiang Xue committed
124
			$primaryKey = static::primaryKey();
Qiang Xue committed
125
			return $query->where(array($primaryKey[0] => $q))->one();
Qiang Xue committed
126
		}
Qiang Xue committed
127
		return $query;
w  
Qiang Xue committed
128 129
	}

Qiang Xue committed
130
	/**
Qiang Xue committed
131 132 133 134 135 136 137 138 139 140 141 142 143
	 * 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
144 145
	 * @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
146
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance
Qiang Xue committed
147
	 */
Qiang Xue committed
148
	public static function findBySql($sql, $params = array())
w  
Qiang Xue committed
149
	{
Qiang Xue committed
150
		$query = static::createQuery();
Qiang Xue committed
151 152 153 154 155 156
		$query->sql = $sql;
		return $query->params($params);
	}

	/**
	 * Updates the whole table using the provided attribute values and conditions.
Qiang Xue committed
157 158 159 160 161 162 163 164
	 * 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
165 166 167 168
	 * 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
169
	public static function updateAll($attributes, $condition = '', $params = array())
w  
Qiang Xue committed
170
	{
Qiang Xue committed
171
		$command = static::getDb()->createCommand();
Qiang Xue committed
172 173
		$command->update(static::tableName(), $attributes, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
174 175
	}

Qiang Xue committed
176
	/**
Qiang Xue committed
177 178 179 180 181 182 183
	 * 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
184
	 * @param array $counters the counters to be updated (attribute name => increment value).
Qiang Xue committed
185 186
	 * 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
187 188
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name=>value) to be bound to the query.
Qiang Xue committed
189
	 * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
Qiang Xue committed
190 191 192
	 * @return integer the number of rows updated
	 */
	public static function updateAllCounters($counters, $condition = '', $params = array())
w  
Qiang Xue committed
193
	{
Qiang Xue committed
194
		$db = static::getDb();
Qiang Xue committed
195
		$n = 0;
Qiang Xue committed
196
		foreach ($counters as $name => $value) {
Qiang Xue committed
197
			$quotedName = $db->quoteColumnName($name);
Qiang Xue committed
198 199
			$counters[$name] = new Expression("$quotedName+:bp{$n}");
			$params[":bp{$n}"] = $value;
Qiang Xue committed
200
			$n++;
Qiang Xue committed
201
		}
Qiang Xue committed
202 203 204
		$command = $db->createCommand();
		$command->update(static::tableName(), $counters, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
205 206
	}

Qiang Xue committed
207 208
	/**
	 * Deletes rows in the table using the provided conditions.
Qiang Xue committed
209 210 211 212 213 214 215 216 217
	 * 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
218 219
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name=>value) to be bound to the query.
Qiang Xue committed
220
	 * @return integer the number of rows deleted
Qiang Xue committed
221
	 */
Qiang Xue committed
222
	public static function deleteAll($condition = '', $params = array())
w  
Qiang Xue committed
223
	{
Qiang Xue committed
224
		$command = static::getDb()->createCommand();
Qiang Xue committed
225 226
		$command->delete(static::tableName(), $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
227 228
	}

.  
Qiang Xue committed
229
	/**
Qiang Xue committed
230 231 232 233
	 * 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
234
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance.
.  
Qiang Xue committed
235
	 */
Qiang Xue committed
236
	public static function createQuery()
w  
Qiang Xue committed
237
	{
Qiang Xue committed
238 239 240
		return new ActiveQuery(array(
			'modelClass' => get_called_class(),
		));
w  
Qiang Xue committed
241 242 243
	}

	/**
Qiang Xue committed
244
	 * Declares the name of the database table associated with this AR class.
Qiang Xue committed
245
	 * By default this method returns the class name as the table name by calling [[StringHelper::camel2id()]]
Qiang Xue committed
246 247
	 * 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
248 249
	 * @return string the table name
	 */
Qiang Xue committed
250
	public static function tableName()
w  
Qiang Xue committed
251
	{
Qiang Xue committed
252
		return 'tbl_' . StringHelper::camel2id(basename(get_called_class()), '_');
w  
Qiang Xue committed
253 254 255
	}

	/**
Qiang Xue committed
256 257
	 * 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
258
	 */
Qiang Xue committed
259
	public static function getTableSchema()
w  
Qiang Xue committed
260
	{
Qiang Xue committed
261
		return static::getDb()->getTableSchema(static::tableName());
w  
Qiang Xue committed
262 263 264
	}

	/**
Qiang Xue committed
265 266
	 * Returns the primary key name(s) for this AR class.
	 * The default implementation will return the primary key(s) as declared
Qiang Xue committed
267
	 * in the DB table that is associated with this AR class.
Qiang Xue committed
268
	 *
Qiang Xue committed
269 270 271
	 * 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
272 273 274
	 *
	 * Note that an array should be returned even for a table with single primary key.
	 *
Qiang Xue committed
275
	 * @return string[] the primary keys of the associated database table.
w  
Qiang Xue committed
276
	 */
Qiang Xue committed
277
	public static function primaryKey()
w  
Qiang Xue committed
278
	{
Qiang Xue committed
279
		return static::getTableSchema()->primaryKey;
w  
Qiang Xue committed
280 281 282
	}

	/**
Qiang Xue committed
283
	 * PHP getter magic method.
Qiang Xue committed
284
	 * This method is overridden so that attributes and related objects can be accessed like properties.
Qiang Xue committed
285 286 287 288 289 290
	 * @param string $name property name
	 * @return mixed property value
	 * @see getAttribute
	 */
	public function __get($name)
	{
Qiang Xue committed
291
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
Qiang Xue committed
292
			return $this->_attributes[$name];
Qiang Xue committed
293
		} elseif (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
294
			return null;
Qiang Xue committed
295 296 297 298 299 300 301 302
		} 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
303
			} else {
Qiang Xue committed
304
				return $value;
Qiang Xue committed
305
			}
Qiang Xue committed
306 307 308 309 310 311 312 313 314 315 316
		}
	}

	/**
	 * 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
317
		if (isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
318 319 320 321 322 323 324 325
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
Qiang Xue committed
326
	 * This method overrides the parent implementation by checking if the named attribute is null or not.
Qiang Xue committed
327 328 329 330
	 * @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
331
	{
Qiang Xue committed
332 333 334 335
		try {
			return $this->__get($name) !== null;
		} catch (\Exception $e) {
			return false;
Qiang Xue committed
336 337 338 339 340 341 342 343 344 345 346
		}
	}

	/**
	 * 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
347
		if (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
348
			unset($this->_attributes[$name]);
Qiang Xue committed
349
		} else {
Qiang Xue committed
350 351 352 353 354 355
			$t = strtolower($name);
			if (isset($this->_related[$t])) {
				unset($this->_related[$t]);
			} else {
				parent::__unset($name);
			}
Qiang Xue committed
356 357 358
		}
	}

Qiang Xue committed
359 360 361 362
	/**
	 * 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
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
	 *
	 * 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
383 384 385 386 387 388
	 * @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
389
	public function hasOne($class, $link)
Qiang Xue committed
390
	{
Qiang Xue committed
391 392 393 394 395 396
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => false,
		));
Qiang Xue committed
397 398
	}

Qiang Xue committed
399 400 401 402
	/**
	 * 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
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
	 *
	 * 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
421 422 423 424 425 426
	 * @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
427
	public function hasMany($class, $link)
Qiang Xue committed
428
	{
Qiang Xue committed
429 430 431 432 433 434
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => true,
		));
Qiang Xue committed
435 436
	}

Qiang Xue committed
437
	/**
Qiang Xue committed
438 439 440 441
	 * 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
442
	 */
Qiang Xue committed
443
	public function populateRelation($name, $records)
Qiang Xue committed
444
	{
Qiang Xue committed
445
		$this->_related[strtolower($name)] = $records;
Qiang Xue committed
446 447
	}

Qiang Xue committed
448 449
	/**
	 * Returns the list of all attribute names of the model.
Qiang Xue committed
450
	 * The default implementation will return all column names of the table associated with this AR class.
Qiang Xue committed
451 452
	 * @return array list of attribute names.
	 */
453
	public function attributes()
Qiang Xue committed
454
	{
Qiang Xue committed
455
		return array_keys($this->getTableSchema()->columns);
456 457
	}

w  
Qiang Xue committed
458 459 460 461 462 463 464 465 466 467
	/**
	 * 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
468
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
w  
Qiang Xue committed
469 470 471 472 473 474 475 476 477 478
	}

	/**
	 * 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
479
		$this->_attributes[$name] = $value;
w  
Qiang Xue committed
480 481
	}

Qiang Xue committed
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
	/**
	 * 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
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
	/**
	 * 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)
	{
		if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) {
			return $this->_attribute[$name] !== $this->_oldAttributes[$name];
		} else {
			return isset($this->_attributes[$name]) || isset($this->_oldAttributes);
		}
	}

Qiang Xue committed
540 541 542 543 544 545
	/**
	 * 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
546
	public function getDirtyAttributes($names = null)
Qiang Xue committed
547 548
	{
		if ($names === null) {
549
			$names = $this->attributes();
Qiang Xue committed
550 551 552
		}
		$names = array_flip($names);
		$attributes = array();
Qiang Xue committed
553
		if ($this->_oldAttributes === null) {
Qiang Xue committed
554 555 556 557 558 559 560 561 562 563
			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
564
			}
Qiang Xue committed
565
		}
Qiang Xue committed
566
		return $attributes;
w  
Qiang Xue committed
567 568 569 570 571
	}

	/**
	 * Saves the current record.
	 *
Qiang Xue committed
572 573 574 575
	 * 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
576
	 *
Qiang Xue committed
577 578 579 580 581 582
	 * ~~~
	 * $customer = new Customer;  // or $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->save();
	 * ~~~
w  
Qiang Xue committed
583 584 585 586 587 588 589 590 591 592
	 *
	 *
	 * @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
593
		return $this->getIsNewRecord() ? $this->insert($runValidation, $attributes) : $this->update($runValidation, $attributes);
Qiang Xue committed
594 595 596
	}

	/**
Qiang Xue committed
597 598 599 600 601 602
	 * 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;
603 604
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
605
	 *    rest of the steps;
606 607
	 * 4. insert the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
608
	 *
609
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
610 611
	 * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
612 613 614 615
	 *
	 * 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
616
	 * it will be populated with the actual value after insertion.
Qiang Xue committed
617 618 619 620 621 622 623 624 625 626 627 628
	 *
	 * 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
629 630 631 632
	 * @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.
	 */
Qiang Xue committed
633
	public function insert($runValidation = true, $attributes = null)
Qiang Xue committed
634
	{
Qiang Xue committed
635 636 637
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
Qiang Xue committed
638
		if ($this->beforeSave(true)) {
Qiang Xue committed
639
			$values = $this->getDirtyAttributes($attributes);
Qiang Xue committed
640 641 642 643 644
			if ($values === array()) {
				foreach ($this->primaryKey() as $key) {
					$values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null;
				}
			}
Qiang Xue committed
645
			$db = static::getDb();
Qiang Xue committed
646
			$command = $db->createCommand()->insert($this->tableName(), $values);
Qiang Xue committed
647
			if ($command->execute()) {
Qiang Xue committed
648
				$table = $this->getTableSchema();
Qiang Xue committed
649 650 651 652 653 654 655 656 657 658 659
				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;
				}
Qiang Xue committed
660
				$this->afterSave(true);
Qiang Xue committed
661 662 663 664 665 666 667
				return true;
			}
		}
		return false;
	}

	/**
Qiang Xue committed
668 669 670 671 672 673
	 * 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;
674 675
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
676
	 *    rest of the steps;
677 678
	 * 4. save the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
679
	 *
680
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
681 682
	 * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
683 684 685 686 687 688 689 690 691 692 693 694 695 696
	 *
	 * 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();
	 * ~~~
	 *
	 * @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
697 698
	 * @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.
Qiang Xue committed
699
	 * @return boolean whether the attributes are valid and the record is updated successfully.
Qiang Xue committed
700
	 */
Qiang Xue committed
701
	public function update($runValidation = true, $attributes = null)
Qiang Xue committed
702
	{
Qiang Xue committed
703 704 705
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
Qiang Xue committed
706
		if ($this->beforeSave(false)) {
Qiang Xue committed
707
			$values = $this->getDirtyAttributes($attributes);
Qiang Xue committed
708
			if ($values !== array()) {
Qiang Xue committed
709 710
				// 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.
Qiang Xue committed
711 712 713 714
				$this->updateAll($values, $this->getOldPrimaryKey(true));
				foreach ($values as $name => $value) {
					$this->_oldAttributes[$name] = $this->_attributes[$name];
				}
Qiang Xue committed
715
				$this->afterSave(false);
Qiang Xue committed
716 717 718 719 720 721 722 723
			}
			return true;
		} else {
			return false;
		}
	}

	/**
Qiang Xue committed
724
	 * Updates one or several counter columns for the current AR object.
Qiang Xue committed
725 726 727 728 729 730
	 * 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
731
	 * $post = Post::find($id);
Qiang Xue committed
732 733 734 735
	 * $post->updateCounters(array('view_count' => 1));
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value)
Qiang Xue committed
736
	 * Use negative values if you want to decrement the counters.
Qiang Xue committed
737 738 739 740 741
	 * @return boolean whether the saving is successful
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
Qiang Xue committed
742 743 744 745 746 747 748 749
		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
750 751 752 753
		}
	}

	/**
Qiang Xue committed
754 755 756 757 758 759 760 761 762
	 * 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
763
	 * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
Qiang Xue committed
764 765
	 * will be raised by the corresponding methods.
	 *
Qiang Xue committed
766 767 768 769 770
	 * @return boolean whether the deletion is successful.
	 */
	public function delete()
	{
		if ($this->beforeDelete()) {
Qiang Xue committed
771 772 773
			// 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
			$this->deleteAll($this->getPrimaryKey(true));
Qiang Xue committed
774 775
			$this->_oldAttributes = null;
			$this->afterDelete();
Qiang Xue committed
776
			return true;
Qiang Xue committed
777
		} else {
w  
Qiang Xue committed
778
			return false;
Qiang Xue committed
779
		}
w  
Qiang Xue committed
780 781 782
	}

	/**
Qiang Xue committed
783
	 * Returns a value indicating whether the current record is new.
Qiang Xue committed
784
	 * @return boolean whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
785 786 787
	 */
	public function getIsNewRecord()
	{
Qiang Xue committed
788
		return $this->_oldAttributes === null;
w  
Qiang Xue committed
789 790
	}

791 792 793
	/**
	 * Initializes the object.
	 * This method is called at the end of the constructor.
Qiang Xue committed
794
	 * The default implementation will trigger an [[EVENT_INIT]] event.
795 796 797 798 799 800
	 * 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();
801
		$this->trigger(self::EVENT_INIT);
802 803 804 805
	}

	/**
	 * This method is called when the AR object is created and populated with the query result.
Qiang Xue committed
806
	 * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
807 808 809 810 811
	 * When overriding this method, make sure you call the parent implementation to ensure the
	 * event is triggered.
	 */
	public function afterFind()
	{
812
		$this->trigger(self::EVENT_AFTER_FIND);
813 814
	}

w  
Qiang Xue committed
815
	/**
Qiang Xue committed
816
	 * Sets the value indicating whether the record is new.
Qiang Xue committed
817
	 * @param boolean $value whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
818 819 820 821
	 * @see getIsNewRecord
	 */
	public function setIsNewRecord($value)
	{
Qiang Xue committed
822
		$this->_oldAttributes = $value ? null : $this->_attributes;
w  
Qiang Xue committed
823 824
	}

Qiang Xue committed
825 826
	/**
	 * This method is called at the beginning of inserting or updating a record.
Qiang Xue committed
827 828
	 * 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
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
	 * 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
848
	public function beforeSave($insert)
w  
Qiang Xue committed
849
	{
Qiang Xue committed
850
		$event = new ModelEvent;
851
		$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
Qiang Xue committed
852
		return $event->isValid;
w  
Qiang Xue committed
853 854
	}

Qiang Xue committed
855 856
	/**
	 * This method is called at the end of inserting or updating a record.
Qiang Xue committed
857 858
	 * 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
859 860 861 862 863
	 * 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
864
	public function afterSave($insert)
w  
Qiang Xue committed
865
	{
866
		$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE);
w  
Qiang Xue committed
867 868 869 870
	}

	/**
	 * This method is invoked before deleting a record.
Qiang Xue committed
871
	 * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
Qiang Xue committed
872 873 874 875 876 877 878 879 880 881 882 883 884 885
	 * 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
886 887
	 * @return boolean whether the record should be deleted. Defaults to true.
	 */
Qiang Xue committed
888
	public function beforeDelete()
w  
Qiang Xue committed
889
	{
Qiang Xue committed
890
		$event = new ModelEvent;
891
		$this->trigger(self::EVENT_BEFORE_DELETE, $event);
Qiang Xue committed
892
		return $event->isValid;
w  
Qiang Xue committed
893 894 895 896
	}

	/**
	 * This method is invoked after deleting a record.
Qiang Xue committed
897
	 * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
w  
Qiang Xue committed
898 899 900
	 * 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
901
	public function afterDelete()
w  
Qiang Xue committed
902
	{
903
		$this->trigger(self::EVENT_AFTER_DELETE);
w  
Qiang Xue committed
904 905 906
	}

	/**
Qiang Xue committed
907
	 * Repopulates this active record with the latest data.
Qiang Xue committed
908
	 * @param array $attributes
Qiang Xue committed
909 910
	 * @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
911
	 */
Qiang Xue committed
912
	public function refresh($attributes = null)
w  
Qiang Xue committed
913
	{
Qiang Xue committed
914
		$record = $this->find($this->getPrimaryKey(true));
Qiang Xue committed
915 916 917 918
		if ($record === null) {
			return false;
		}
		if ($attributes === null) {
919
			foreach ($this->attributes() as $name) {
Qiang Xue committed
920
				$this->_attributes[$name] = $record->_attributes[$name];
Qiang Xue committed
921
			}
Qiang Xue committed
922
			$this->_oldAttributes = $this->_attributes;
Qiang Xue committed
923
		} else {
Qiang Xue committed
924 925 926
			foreach ($attributes as $name) {
				$this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name];
			}
w  
Qiang Xue committed
927
		}
Qiang Xue committed
928
		return true;
w  
Qiang Xue committed
929 930 931
	}

	/**
Qiang Xue committed
932 933
	 * 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
934
	 * @param ActiveRecord $record record to compare to
Qiang Xue committed
935
	 * @return boolean whether the two active records refer to the same row in the same database table.
w  
Qiang Xue committed
936
	 */
Qiang Xue committed
937
	public function equals($record)
w  
Qiang Xue committed
938
	{
Qiang Xue committed
939
		return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
w  
Qiang Xue committed
940 941 942
	}

	/**
Qiang Xue committed
943
	 * Returns the primary key value(s).
Qiang Xue committed
944
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
Qiang Xue committed
945
	 * the return value will be an array with column names as keys and column values as values.
946
	 * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
Qiang Xue committed
947 948 949
	 * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key
	 * 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
950
	 */
Qiang Xue committed
951
	public function getPrimaryKey($asArray = false)
w  
Qiang Xue committed
952
	{
Qiang Xue committed
953 954 955
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
Qiang Xue committed
956
		} else {
Qiang Xue committed
957
			$values = array();
Qiang Xue committed
958
			foreach ($keys as $name) {
Qiang Xue committed
959
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
Qiang Xue committed
960 961
			}
			return $values;
w  
Qiang Xue committed
962 963 964 965
		}
	}

	/**
Qiang Xue committed
966
	 * Returns the old primary key value(s).
Qiang Xue committed
967 968 969
	 * 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
970 971
	 * @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
972
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
Qiang Xue committed
973 974 975
	 * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key
	 * 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
976
	 */
Qiang Xue committed
977
	public function getOldPrimaryKey($asArray = false)
w  
Qiang Xue committed
978
	{
Qiang Xue committed
979 980 981
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
Qiang Xue committed
982 983
		} else {
			$values = array();
Qiang Xue committed
984
			foreach ($keys as $name) {
Qiang Xue committed
985 986 987 988
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
w  
Qiang Xue committed
989 990 991
	}

	/**
Qiang Xue committed
992
	 * Creates an active record object using a row of data.
Qiang Xue committed
993 994
	 * This method is called by [[ActiveQuery]] to populate the query results
	 * into Active Records.
Qiang Xue committed
995 996
	 * @param array $row attribute values (name => value)
	 * @return ActiveRecord the newly created active record.
w  
Qiang Xue committed
997
	 */
Qiang Xue committed
998
	public static function create($row)
w  
Qiang Xue committed
999
	{
Qiang Xue committed
1000
		$record = static::instantiate($row);
1001
		$columns = static::getTableSchema()->columns;
Qiang Xue committed
1002
		foreach ($row as $name => $value) {
Qiang Xue committed
1003
			if (isset($columns[$name])) {
Qiang Xue committed
1004
				$record->_attributes[$name] = $value;
Qiang Xue committed
1005
			} else {
Qiang Xue committed
1006
				$record->$name = $value;
w  
Qiang Xue committed
1007 1008
			}
		}
Qiang Xue committed
1009
		$record->_oldAttributes = $record->_attributes;
1010
		$record->afterFind();
Qiang Xue committed
1011
		return $record;
w  
Qiang Xue committed
1012 1013 1014 1015
	}

	/**
	 * Creates an active record instance.
Qiang Xue committed
1016
	 * This method is called by [[create()]].
w  
Qiang Xue committed
1017
	 * You may override this method if the instance being created
Qiang Xue committed
1018
	 * depends on the row data to be populated into the record.
w  
Qiang Xue committed
1019 1020
	 * 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
1021 1022
	 * @param array $row row data to be populated into the record.
	 * @return ActiveRecord the newly created active record
w  
Qiang Xue committed
1023
	 */
Qiang Xue committed
1024
	public static function instantiate($row)
w  
Qiang Xue committed
1025
	{
Qiang Xue committed
1026
		return new static;
w  
Qiang Xue committed
1027 1028 1029 1030 1031 1032
	}

	/**
	 * 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
1033
	 * @return boolean whether there is an element at the specified offset.
w  
Qiang Xue committed
1034 1035 1036 1037 1038
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}
Qiang Xue committed
1039

Qiang Xue committed
1040
	/**
Qiang Xue committed
1041 1042 1043 1044 1045
	 * 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
1046
	 * @throws InvalidParamException if the named relation does not exist.
Qiang Xue committed
1047 1048 1049 1050 1051 1052 1053 1054 1055
	 */
	public function getRelation($name)
	{
		$getter = 'get' . $name;
		try {
			$relation = $this->$getter();
			if ($relation instanceof ActiveRelation) {
				return $relation;
			}
Qiang Xue committed
1056
		} catch (UnknownMethodException $e) {
Qiang Xue committed
1057
		}
Qiang Xue committed
1058
		throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
Qiang Xue committed
1059 1060
	}

Qiang Xue committed
1061
	/**
Qiang Xue committed
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
	 * 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
1078
	 * @throws InvalidCallException if the method is unable to link two models.
Qiang Xue committed
1079
	 */
Qiang Xue committed
1080
	public function link($name, $model, $extraColumns = array())
Qiang Xue committed
1081
	{
1082 1083 1084 1085
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1086 1087
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1088
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1089
				$viaClass = $viaRelation->modelClass;
1090
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1091
				// unset $viaName so that it can be reloaded to reflect the change
Qiang Xue committed
1092
				unset($this->_related[strtolower($viaName)]);
1093
			} else {
Qiang Xue committed
1094
				$viaRelation = $relation->via;
1095 1096 1097
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1098
			foreach ($viaRelation->link as $a => $b) {
1099 1100 1101 1102 1103
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1104
			foreach ($extraColumns as $k => $v) {
1105 1106
				$columns[$k] = $v;
			}
Qiang Xue committed
1107
			static::getDb()->createCommand()
Qiang Xue committed
1108 1109 1110 1111 1112 1113
				->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
1114
					throw new InvalidCallException('Unable to link models: both models are newly created.');
Qiang Xue committed
1115 1116
				} elseif ($this->getIsNewRecord()) {
					$this->bindModels(array_flip($relation->link), $this, $model);
Qiang Xue committed
1117
				} else {
Qiang Xue committed
1118
					$this->bindModels($relation->link, $model, $this);
1119
				}
Qiang Xue committed
1120 1121 1122 1123
			} elseif ($p1) {
				$this->bindModels(array_flip($relation->link), $this, $model);
			} elseif ($p2) {
				$this->bindModels($relation->link, $model, $this);
1124
			} else {
Qiang Xue committed
1125
				throw new InvalidCallException('Unable to link models: the link does not involve any primary key.');
1126 1127
			}
		}
Qiang Xue committed
1128

Qiang Xue committed
1129
		// update lazily loaded related objects
Qiang Xue committed
1130 1131 1132 1133 1134 1135 1136 1137 1138 1139
		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;
			}
		}
1140 1141 1142
	}

	/**
Qiang Xue committed
1143 1144 1145 1146 1147 1148 1149
	 * 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
1150 1151
	 * @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
1152
	 * If true, the model containing the foreign key will be deleted.
Qiang Xue committed
1153
	 * @throws InvalidCallException if the models cannot be unlinked
1154
	 */
Qiang Xue committed
1155
	public function unlink($name, $model, $delete = false)
1156 1157 1158 1159 1160
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1161 1162
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1163
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1164
				$viaClass = $viaRelation->modelClass;
1165
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1166
				unset($this->_related[strtolower($viaName)]);
1167
			} else {
Qiang Xue committed
1168
				$viaRelation = $relation->via;
1169 1170 1171
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1172
			foreach ($viaRelation->link as $a => $b) {
1173 1174 1175 1176 1177
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1178
			$command = static::getDb()->createCommand();
Qiang Xue committed
1179 1180 1181 1182 1183 1184 1185 1186
			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();
1187 1188
			}
		} else {
Qiang Xue committed
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201
			$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
1202
				throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
Qiang Xue committed
1203
			}
1204
		}
Qiang Xue committed
1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215

		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]);
				}
			}
		}
1216 1217
	}

Qiang Xue committed
1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236
	/**
	 * 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;
	}

1237
	/**
Qiang Xue committed
1238 1239 1240
	 * @param array $link
	 * @param ActiveRecord $foreignModel
	 * @param ActiveRecord $primaryModel
Qiang Xue committed
1241
	 * @throws InvalidCallException
1242
	 */
Qiang Xue committed
1243
	private function bindModels($link, $foreignModel, $primaryModel)
1244
	{
Qiang Xue committed
1245 1246 1247
		foreach ($link as $fk => $pk) {
			$value = $primaryModel->$pk;
			if ($value === null) {
Qiang Xue committed
1248
				throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
Qiang Xue committed
1249
			}
Qiang Xue committed
1250
			$foreignModel->$fk = $value;
Qiang Xue committed
1251
		}
Qiang Xue committed
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267
		$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
1268
	}
w  
Qiang Xue committed
1269
}