ActiveRecord.php 45.5 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;
w  
Qiang Xue committed
21

w  
Qiang Xue committed
22
/**
Qiang Xue committed
23
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
w  
Qiang Xue committed
24
 *
Qiang Xue committed
25
 * @include @yii/db/ActiveRecord.md
w  
Qiang Xue committed
26
 *
Qiang Xue committed
27
 * @property Connection $db the database connection used by this AR class.
Qiang Xue committed
28 29 30
 * @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).
31
 * @property boolean $isNewRecord whether the record is new and should be inserted when calling [[save()]].
Qiang Xue committed
32 33 34
 * @property mixed $primaryKey the primary key value.
 * @property mixed $oldPrimaryKey the old primary key value.
 *
Qiang Xue committed
35 36
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
w  
Qiang Xue committed
37
 */
Qiang Xue committed
38
class ActiveRecord extends Model
w  
Qiang Xue committed
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 75
	/**
	 * @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
76
	/**
Qiang Xue committed
77 78 79 80 81
	 * @var array attribute values indexed by attribute names
	 */
	private $_attributes = array();
	/**
	 * @var array old attribute values indexed by attribute names.
w  
Qiang Xue committed
82
	 */
Qiang Xue committed
83
	private $_oldAttributes;
84
	/**
Qiang Xue committed
85
	 * @var array related models indexed by the relation names
86
	 */
Qiang Xue committed
87 88
	private $_related;

89

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

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

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

	/**
	 * Updates the whole table using the provided attribute values and conditions.
Qiang Xue committed
163 164 165 166 167 168 169 170
	 * 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
171
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
172
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
173 174
	 * @return integer the number of rows updated
	 */
Qiang Xue committed
175
	public static function updateAll($attributes, $condition = '', $params = array())
w  
Qiang Xue committed
176
	{
Qiang Xue committed
177
		$command = static::getDb()->createCommand();
Qiang Xue committed
178 179
		$command->update(static::tableName(), $attributes, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
180 181
	}

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

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

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

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

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

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

285
	/**
286
	 * Returns the name of the column that stores the lock version for implementing optimistic locking.
287
	 *
288 289 290 291
	 * 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.
292 293 294 295 296
	 *
	 * Optimized locking is only supported by [[update()]] and [[delete()]].
	 *
	 * To use optimized locking:
	 *
297
	 * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
298
	 *    Override this method to return the name of this column.
299 300 301 302 303 304 305 306 307
	 * 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.
	 */
308
	public function optimisticLock()
309 310 311 312
	{
		return null;
	}

w  
Qiang Xue committed
313
	/**
Qiang Xue committed
314
	 * PHP getter magic method.
Qiang Xue committed
315
	 * This method is overridden so that attributes and related objects can be accessed like properties.
Qiang Xue committed
316 317 318 319 320 321
	 * @param string $name property name
	 * @return mixed property value
	 * @see getAttribute
	 */
	public function __get($name)
	{
Qiang Xue committed
322
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
Qiang Xue committed
323
			return $this->_attributes[$name];
Qiang Xue committed
324
		} elseif (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
325
			return null;
Qiang Xue committed
326 327 328 329 330 331 332 333
		} 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
334
			} else {
Qiang Xue committed
335
				return $value;
Qiang Xue committed
336
			}
Qiang Xue committed
337 338 339 340 341 342 343 344 345 346 347
		}
	}

	/**
	 * 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
348
		if (isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
349 350 351 352 353 354 355 356
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
Qiang Xue committed
357
	 * This method overrides the parent implementation by checking if the named attribute is null or not.
Qiang Xue committed
358 359 360 361
	 * @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
362
	{
Qiang Xue committed
363 364 365 366
		try {
			return $this->__get($name) !== null;
		} catch (\Exception $e) {
			return false;
Qiang Xue committed
367 368 369 370 371 372 373 374 375 376 377
		}
	}

	/**
	 * 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
378
		if (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
379
			unset($this->_attributes[$name]);
Qiang Xue committed
380
		} else {
Qiang Xue committed
381 382 383 384 385 386
			$t = strtolower($name);
			if (isset($this->_related[$t])) {
				unset($this->_related[$t]);
			} else {
				parent::__unset($name);
			}
Qiang Xue committed
387 388 389
		}
	}

Qiang Xue committed
390 391 392 393
	/**
	 * 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
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
	 *
	 * 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
414 415 416 417 418 419
	 * @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
420
	public function hasOne($class, $link)
Qiang Xue committed
421
	{
Qiang Xue committed
422 423 424 425 426 427
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => false,
		));
Qiang Xue committed
428 429
	}

Qiang Xue committed
430 431 432 433
	/**
	 * 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
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
	 *
	 * 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
452 453 454 455 456 457
	 * @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
458
	public function hasMany($class, $link)
Qiang Xue committed
459
	{
Qiang Xue committed
460 461 462 463 464 465
		return new ActiveRelation(array(
			'modelClass' => $this->getNamespacedClass($class),
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => true,
		));
Qiang Xue committed
466 467
	}

Qiang Xue committed
468
	/**
Qiang Xue committed
469 470 471 472
	 * 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
473
	 */
Qiang Xue committed
474
	public function populateRelation($name, $records)
Qiang Xue committed
475
	{
Qiang Xue committed
476
		$this->_related[strtolower($name)] = $records;
Qiang Xue committed
477 478
	}

Qiang Xue committed
479 480
	/**
	 * Returns the list of all attribute names of the model.
Qiang Xue committed
481
	 * The default implementation will return all column names of the table associated with this AR class.
Qiang Xue committed
482 483
	 * @return array list of attribute names.
	 */
484
	public function attributes()
Qiang Xue committed
485
	{
Qiang Xue committed
486
		return array_keys($this->getTableSchema()->columns);
487 488
	}

w  
Qiang Xue committed
489 490 491 492 493 494 495 496 497 498
	/**
	 * 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
499
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
w  
Qiang Xue committed
500 501 502 503 504 505 506 507 508 509
	}

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

Qiang Xue committed
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
	/**
	 * 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
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
	/**
	 * 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)
	{
564 565
		if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
			return $this->_attributes[$name] !== $this->_oldAttributes[$name];
Qiang Xue committed
566 567 568 569 570
		} else {
			return isset($this->_attributes[$name]) || isset($this->_oldAttributes);
		}
	}

Qiang Xue committed
571 572 573 574 575 576
	/**
	 * 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
577
	public function getDirtyAttributes($names = null)
Qiang Xue committed
578 579
	{
		if ($names === null) {
580
			$names = $this->attributes();
Qiang Xue committed
581 582 583
		}
		$names = array_flip($names);
		$attributes = array();
Qiang Xue committed
584
		if ($this->_oldAttributes === null) {
Qiang Xue committed
585 586 587 588 589 590 591 592 593 594
			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
595
			}
Qiang Xue committed
596
		}
Qiang Xue committed
597
		return $attributes;
w  
Qiang Xue committed
598 599 600 601 602
	}

	/**
	 * Saves the current record.
	 *
Qiang Xue committed
603 604 605 606
	 * 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
607
	 *
Qiang Xue committed
608 609 610 611 612 613
	 * ~~~
	 * $customer = new Customer;  // or $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->save();
	 * ~~~
w  
Qiang Xue committed
614 615 616 617 618 619 620 621 622 623
	 *
	 *
	 * @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)
	{
624 625 626 627 628
		if ($this->getIsNewRecord()) {
			return $this->insert($runValidation, $attributes);
		} else {
			return $this->update($runValidation, $attributes) !== false;
		}
Qiang Xue committed
629 630 631
	}

	/**
Qiang Xue committed
632 633 634 635 636 637
	 * 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;
638 639
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
640
	 *    rest of the steps;
641 642
	 * 4. insert the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
643
	 *
644
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
645 646
	 * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
647 648 649 650
	 *
	 * 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
651
	 * it will be populated with the actual value after insertion.
Qiang Xue committed
652 653 654 655 656 657 658 659 660 661 662 663
	 *
	 * 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
664 665 666 667
	 * @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
668
	public function insert($runValidation = true, $attributes = null)
Qiang Xue committed
669
	{
Alexander Kochetov committed
670
		if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(true)) {
Qiang Xue committed
671 672
			return false;
		}
673 674 675 676
		$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
677
			}
678 679 680 681 682 683 684 685 686 687
		}
		$db = static::getDb();
		$command = $db->createCommand()->insert($this->tableName(), $values);
		if ($command->execute()) {
			$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
688 689 690
					}
				}
			}
691 692 693 694 695
			foreach ($values as $name => $value) {
				$this->_oldAttributes[$name] = $value;
			}
			$this->afterSave(true);
			return true;
Qiang Xue committed
696 697 698 699
		}
	}

	/**
Qiang Xue committed
700 701 702 703 704 705
	 * 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;
706 707
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
708
	 *    rest of the steps;
709 710
	 * 4. save the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
711
	 *
712
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
713 714
	 * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
715 716 717 718 719 720 721 722 723 724 725 726
	 *
	 * 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();
	 * ~~~
	 *
727 728 729 730 731 732 733 734 735 736 737 738
	 * 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
739 740
	 * @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
741 742
	 * @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.
743 744
	 * @return integer|boolean the number of rows affected, or false if validation fails
	 * or [[beforeSave()]] stops the updating process.
745
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
746
	 * being updated is outdated.
Qiang Xue committed
747
	 */
Qiang Xue committed
748
	public function update($runValidation = true, $attributes = null)
Qiang Xue committed
749
	{
Alexander Kochetov committed
750
		if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(false)) {
Qiang Xue committed
751 752
			return false;
		}
753 754 755 756 757 758 759
		$values = $this->getDirtyAttributes($attributes);
		if (!empty($values)) {
			$condition = $this->getOldPrimaryKey(true);
			$lock = $this->optimisticLock();
			if ($lock !== null) {
				if (!isset($values[$lock])) {
					$values[$lock] = $this->$lock + 1;
760
				}
761 762 763 764 765
				$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);
766

767 768 769
			if ($lock !== null && !$rows) {
				throw new StaleObjectException('The object being updated is outdated.');
			}
770

771 772
			foreach ($values as $name => $value) {
				$this->_oldAttributes[$name] = $this->_attributes[$name];
Qiang Xue committed
773
			}
774 775 776

			$this->afterSave(false);
			return $rows;
Qiang Xue committed
777
		} else {
778
			return 0;
Qiang Xue committed
779 780 781 782
		}
	}

	/**
Qiang Xue committed
783
	 * Updates one or several counter columns for the current AR object.
Qiang Xue committed
784 785 786 787 788 789
	 * 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
790
	 * $post = Post::find($id);
Qiang Xue committed
791 792 793 794
	 * $post->updateCounters(array('view_count' => 1));
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value)
Qiang Xue committed
795
	 * Use negative values if you want to decrement the counters.
Qiang Xue committed
796 797 798 799 800
	 * @return boolean whether the saving is successful
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
Qiang Xue committed
801 802 803 804 805 806 807 808
		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
809 810 811 812
		}
	}

	/**
Qiang Xue committed
813 814 815 816 817 818 819 820 821
	 * 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
822
	 * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
Qiang Xue committed
823 824
	 * will be raised by the corresponding methods.
	 *
825 826
	 * @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.
827
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
828
	 * being deleted is outdated.
Qiang Xue committed
829 830 831 832
	 */
	public function delete()
	{
		if ($this->beforeDelete()) {
Qiang Xue committed
833 834
			// 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
835
			$condition = $this->getOldPrimaryKey(true);
836
			$lock = $this->optimisticLock();
837 838 839 840 841 842 843
			if ($lock !== null) {
				$condition[$lock] = $this->$lock;
			}
			$rows = $this->deleteAll($condition);
			if ($lock !== null && !$rows) {
				throw new StaleObjectException('The object being deleted is outdated.');
			}
Qiang Xue committed
844 845
			$this->_oldAttributes = null;
			$this->afterDelete();
846
			return $rows;
Qiang Xue committed
847
		} else {
w  
Qiang Xue committed
848
			return false;
Qiang Xue committed
849
		}
w  
Qiang Xue committed
850 851 852
	}

	/**
Qiang Xue committed
853
	 * Returns a value indicating whether the current record is new.
Qiang Xue committed
854
	 * @return boolean whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
855 856 857
	 */
	public function getIsNewRecord()
	{
Qiang Xue committed
858
		return $this->_oldAttributes === null;
w  
Qiang Xue committed
859 860
	}

861 862 863
	/**
	 * Initializes the object.
	 * This method is called at the end of the constructor.
Qiang Xue committed
864
	 * The default implementation will trigger an [[EVENT_INIT]] event.
865 866 867 868 869 870
	 * 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();
871
		$this->trigger(self::EVENT_INIT);
872 873 874 875
	}

	/**
	 * This method is called when the AR object is created and populated with the query result.
Qiang Xue committed
876
	 * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
877 878 879 880 881
	 * When overriding this method, make sure you call the parent implementation to ensure the
	 * event is triggered.
	 */
	public function afterFind()
	{
882
		$this->trigger(self::EVENT_AFTER_FIND);
883 884
	}

w  
Qiang Xue committed
885
	/**
Qiang Xue committed
886
	 * Sets the value indicating whether the record is new.
Qiang Xue committed
887
	 * @param boolean $value whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
888 889 890 891
	 * @see getIsNewRecord
	 */
	public function setIsNewRecord($value)
	{
Qiang Xue committed
892
		$this->_oldAttributes = $value ? null : $this->_attributes;
w  
Qiang Xue committed
893 894
	}

Qiang Xue committed
895 896
	/**
	 * This method is called at the beginning of inserting or updating a record.
Qiang Xue committed
897 898
	 * 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
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
	 * 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
918
	public function beforeSave($insert)
w  
Qiang Xue committed
919
	{
Qiang Xue committed
920
		$event = new ModelEvent;
921
		$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
Qiang Xue committed
922
		return $event->isValid;
w  
Qiang Xue committed
923 924
	}

Qiang Xue committed
925 926
	/**
	 * This method is called at the end of inserting or updating a record.
Qiang Xue committed
927 928
	 * 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
929 930 931 932 933
	 * 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
934
	public function afterSave($insert)
w  
Qiang Xue committed
935
	{
936
		$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE);
w  
Qiang Xue committed
937 938 939 940
	}

	/**
	 * This method is invoked before deleting a record.
Qiang Xue committed
941
	 * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
Qiang Xue committed
942 943 944 945 946 947 948 949 950 951 952 953 954 955
	 * 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
956 957
	 * @return boolean whether the record should be deleted. Defaults to true.
	 */
Qiang Xue committed
958
	public function beforeDelete()
w  
Qiang Xue committed
959
	{
Qiang Xue committed
960
		$event = new ModelEvent;
961
		$this->trigger(self::EVENT_BEFORE_DELETE, $event);
Qiang Xue committed
962
		return $event->isValid;
w  
Qiang Xue committed
963 964 965 966
	}

	/**
	 * This method is invoked after deleting a record.
Qiang Xue committed
967
	 * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
w  
Qiang Xue committed
968 969 970
	 * 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
971
	public function afterDelete()
w  
Qiang Xue committed
972
	{
973
		$this->trigger(self::EVENT_AFTER_DELETE);
w  
Qiang Xue committed
974 975 976
	}

	/**
Qiang Xue committed
977
	 * Repopulates this active record with the latest data.
Qiang Xue committed
978
	 * @param array $attributes
Qiang Xue committed
979 980
	 * @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
981
	 */
Qiang Xue committed
982
	public function refresh($attributes = null)
w  
Qiang Xue committed
983
	{
Qiang Xue committed
984
		$record = $this->find($this->getPrimaryKey(true));
Qiang Xue committed
985 986 987 988
		if ($record === null) {
			return false;
		}
		if ($attributes === null) {
989
			foreach ($this->attributes() as $name) {
Qiang Xue committed
990
				$this->_attributes[$name] = $record->_attributes[$name];
Qiang Xue committed
991
			}
Qiang Xue committed
992
			$this->_oldAttributes = $this->_attributes;
Qiang Xue committed
993
		} else {
Qiang Xue committed
994 995 996
			foreach ($attributes as $name) {
				$this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name];
			}
w  
Qiang Xue committed
997
		}
Qiang Xue committed
998
		return true;
w  
Qiang Xue committed
999 1000 1001
	}

	/**
Qiang Xue committed
1002 1003
	 * 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
1004
	 * @param ActiveRecord $record record to compare to
Qiang Xue committed
1005
	 * @return boolean whether the two active records refer to the same row in the same database table.
w  
Qiang Xue committed
1006
	 */
Qiang Xue committed
1007
	public function equals($record)
w  
Qiang Xue committed
1008
	{
Qiang Xue committed
1009
		return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
w  
Qiang Xue committed
1010 1011 1012
	}

	/**
Qiang Xue committed
1013
	 * Returns the primary key value(s).
Qiang Xue committed
1014
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
Qiang Xue committed
1015
	 * the return value will be an array with column names as keys and column values as values.
1016
	 * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
resurtm committed
1017
	 * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1018 1019
	 * 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
1020
	 */
Qiang Xue committed
1021
	public function getPrimaryKey($asArray = false)
w  
Qiang Xue committed
1022
	{
Qiang Xue committed
1023 1024 1025
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
Qiang Xue committed
1026
		} else {
Qiang Xue committed
1027
			$values = array();
Qiang Xue committed
1028
			foreach ($keys as $name) {
Qiang Xue committed
1029
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
Qiang Xue committed
1030 1031
			}
			return $values;
w  
Qiang Xue committed
1032 1033 1034 1035
		}
	}

	/**
Qiang Xue committed
1036
	 * Returns the old primary key value(s).
Qiang Xue committed
1037 1038 1039
	 * 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
1040 1041
	 * @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
1042
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
resurtm committed
1043
	 * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1044 1045
	 * 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
1046
	 */
Qiang Xue committed
1047
	public function getOldPrimaryKey($asArray = false)
w  
Qiang Xue committed
1048
	{
Qiang Xue committed
1049 1050 1051
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
Qiang Xue committed
1052 1053
		} else {
			$values = array();
Qiang Xue committed
1054
			foreach ($keys as $name) {
Qiang Xue committed
1055 1056 1057 1058
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
w  
Qiang Xue committed
1059 1060 1061
	}

	/**
Qiang Xue committed
1062
	 * Creates an active record object using a row of data.
Qiang Xue committed
1063 1064
	 * This method is called by [[ActiveQuery]] to populate the query results
	 * into Active Records.
Qiang Xue committed
1065 1066
	 * @param array $row attribute values (name => value)
	 * @return ActiveRecord the newly created active record.
w  
Qiang Xue committed
1067
	 */
Qiang Xue committed
1068
	public static function create($row)
w  
Qiang Xue committed
1069
	{
Qiang Xue committed
1070
		$record = static::instantiate($row);
1071
		$columns = static::getTableSchema()->columns;
Qiang Xue committed
1072
		foreach ($row as $name => $value) {
Qiang Xue committed
1073
			if (isset($columns[$name])) {
Qiang Xue committed
1074
				$record->_attributes[$name] = $value;
Qiang Xue committed
1075
			} else {
Qiang Xue committed
1076
				$record->$name = $value;
w  
Qiang Xue committed
1077 1078
			}
		}
Qiang Xue committed
1079
		$record->_oldAttributes = $record->_attributes;
1080
		$record->afterFind();
Qiang Xue committed
1081
		return $record;
w  
Qiang Xue committed
1082 1083 1084 1085
	}

	/**
	 * Creates an active record instance.
Qiang Xue committed
1086
	 * This method is called by [[create()]].
w  
Qiang Xue committed
1087
	 * You may override this method if the instance being created
Qiang Xue committed
1088
	 * depends on the row data to be populated into the record.
w  
Qiang Xue committed
1089 1090
	 * 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
1091 1092
	 * @param array $row row data to be populated into the record.
	 * @return ActiveRecord the newly created active record
w  
Qiang Xue committed
1093
	 */
Qiang Xue committed
1094
	public static function instantiate($row)
w  
Qiang Xue committed
1095
	{
Qiang Xue committed
1096
		return new static;
w  
Qiang Xue committed
1097 1098 1099 1100 1101 1102
	}

	/**
	 * 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
1103
	 * @return boolean whether there is an element at the specified offset.
w  
Qiang Xue committed
1104 1105 1106 1107 1108
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}
Qiang Xue committed
1109

Qiang Xue committed
1110
	/**
Qiang Xue committed
1111 1112 1113 1114 1115
	 * 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
1116
	 * @throws InvalidParamException if the named relation does not exist.
Qiang Xue committed
1117 1118 1119 1120 1121 1122 1123 1124 1125
	 */
	public function getRelation($name)
	{
		$getter = 'get' . $name;
		try {
			$relation = $this->$getter();
			if ($relation instanceof ActiveRelation) {
				return $relation;
			}
Qiang Xue committed
1126
		} catch (UnknownMethodException $e) {
1127
			throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
Qiang Xue committed
1128 1129 1130
		}
	}

Qiang Xue committed
1131
	/**
Qiang Xue committed
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147
	 * 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
1148
	 * @throws InvalidCallException if the method is unable to link two models.
Qiang Xue committed
1149
	 */
Qiang Xue committed
1150
	public function link($name, $model, $extraColumns = array())
Qiang Xue committed
1151
	{
1152 1153 1154 1155
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1156 1157
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1158
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1159
				$viaClass = $viaRelation->modelClass;
1160
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1161
				// unset $viaName so that it can be reloaded to reflect the change
Qiang Xue committed
1162
				unset($this->_related[strtolower($viaName)]);
1163
			} else {
Qiang Xue committed
1164
				$viaRelation = $relation->via;
1165 1166 1167
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1168
			foreach ($viaRelation->link as $a => $b) {
1169 1170 1171 1172 1173
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1174
			foreach ($extraColumns as $k => $v) {
1175 1176
				$columns[$k] = $v;
			}
Qiang Xue committed
1177
			static::getDb()->createCommand()
Qiang Xue committed
1178 1179 1180 1181 1182 1183
				->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
1184
					throw new InvalidCallException('Unable to link models: both models are newly created.');
Qiang Xue committed
1185 1186
				} elseif ($this->getIsNewRecord()) {
					$this->bindModels(array_flip($relation->link), $this, $model);
Qiang Xue committed
1187
				} else {
Qiang Xue committed
1188
					$this->bindModels($relation->link, $model, $this);
1189
				}
Qiang Xue committed
1190 1191 1192 1193
			} elseif ($p1) {
				$this->bindModels(array_flip($relation->link), $this, $model);
			} elseif ($p2) {
				$this->bindModels($relation->link, $model, $this);
1194
			} else {
Qiang Xue committed
1195
				throw new InvalidCallException('Unable to link models: the link does not involve any primary key.');
1196 1197
			}
		}
Qiang Xue committed
1198

Qiang Xue committed
1199
		// update lazily loaded related objects
Qiang Xue committed
1200 1201 1202 1203 1204 1205 1206 1207 1208 1209
		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;
			}
		}
1210 1211 1212
	}

	/**
Qiang Xue committed
1213 1214 1215 1216 1217 1218 1219
	 * 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
1220 1221
	 * @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
1222
	 * If true, the model containing the foreign key will be deleted.
Qiang Xue committed
1223
	 * @throws InvalidCallException if the models cannot be unlinked
1224
	 */
Qiang Xue committed
1225
	public function unlink($name, $model, $delete = false)
1226 1227 1228 1229 1230
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1231 1232
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1233
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1234
				$viaClass = $viaRelation->modelClass;
1235
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1236
				unset($this->_related[strtolower($viaName)]);
1237
			} else {
Qiang Xue committed
1238
				$viaRelation = $relation->via;
1239 1240 1241
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1242
			foreach ($viaRelation->link as $a => $b) {
1243 1244 1245 1246 1247
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1248
			$command = static::getDb()->createCommand();
Qiang Xue committed
1249 1250 1251 1252 1253 1254 1255 1256
			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();
1257 1258
			}
		} else {
Qiang Xue committed
1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271
			$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
1272
				throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
Qiang Xue committed
1273
			}
1274
		}
Qiang Xue committed
1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285

		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]);
				}
			}
		}
1286 1287
	}

Qiang Xue committed
1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306
	/**
	 * 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;
	}

1307
	/**
Qiang Xue committed
1308 1309 1310
	 * @param array $link
	 * @param ActiveRecord $foreignModel
	 * @param ActiveRecord $primaryModel
Qiang Xue committed
1311
	 * @throws InvalidCallException
1312
	 */
Qiang Xue committed
1313
	private function bindModels($link, $foreignModel, $primaryModel)
1314
	{
Qiang Xue committed
1315 1316 1317
		foreach ($link as $fk => $pk) {
			$value = $primaryModel->$pk;
			if ($value === null) {
Qiang Xue committed
1318
				throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
Qiang Xue committed
1319
			}
Qiang Xue committed
1320
			$foreignModel->$fk = $value;
Qiang Xue committed
1321
		}
Qiang Xue committed
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
		$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
1338
	}
w  
Qiang Xue committed
1339
}