ActiveRecord.php 51.1 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
use yii\helpers\StringHelper;
18
use yii\helpers\Inflector;
w  
Qiang Xue committed
19

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

77
	/**
78
	 * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
79
	 */
80
	const OP_INSERT = 0x01;
81
	/**
82
	 * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
83
	 */
84
	const OP_UPDATE = 0x02;
85
	/**
86
	 * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
87
	 */
88 89 90 91 92 93
	const OP_DELETE = 0x04;
	/**
	 * All three operations: insert, update, delete.
	 * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
	 */
	const OP_ALL = 0x07;
94

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

108

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

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

Qiang Xue committed
155
	/**
Qiang Xue committed
156 157 158 159 160 161 162 163 164 165 166 167 168
	 * 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
169 170
	 * @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
171
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance
Qiang Xue committed
172
	 */
Qiang Xue committed
173
	public static function findBySql($sql, $params = array())
w  
Qiang Xue committed
174
	{
Qiang Xue committed
175
		$query = static::createQuery();
Qiang Xue committed
176 177 178 179 180 181
		$query->sql = $sql;
		return $query->params($params);
	}

	/**
	 * Updates the whole table using the provided attribute values and conditions.
Qiang Xue committed
182 183 184 185 186 187 188 189
	 * 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
190
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
191
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
192 193
	 * @return integer the number of rows updated
	 */
Qiang Xue committed
194
	public static function updateAll($attributes, $condition = '', $params = array())
w  
Qiang Xue committed
195
	{
Qiang Xue committed
196
		$command = static::getDb()->createCommand();
Qiang Xue committed
197 198
		$command->update(static::tableName(), $attributes, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
199 200
	}

Qiang Xue committed
201
	/**
Qiang Xue committed
202 203 204 205 206 207 208
	 * 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
209
	 * @param array $counters the counters to be updated (attribute name => increment value).
Qiang Xue committed
210 211
	 * 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
212
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
213
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
214
	 * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
Qiang Xue committed
215 216 217
	 * @return integer the number of rows updated
	 */
	public static function updateAllCounters($counters, $condition = '', $params = array())
w  
Qiang Xue committed
218
	{
Qiang Xue committed
219
		$n = 0;
Qiang Xue committed
220
		foreach ($counters as $name => $value) {
221
			$counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value));
Qiang Xue committed
222
			$n++;
Qiang Xue committed
223
		}
224
		$command = static::getDb()->createCommand();
Qiang Xue committed
225 226
		$command->update(static::tableName(), $counters, $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
227 228
	}

Qiang Xue committed
229 230
	/**
	 * Deletes rows in the table using the provided conditions.
Qiang Xue committed
231 232 233 234 235 236 237 238 239
	 * 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
240
	 * Please refer to [[Query::where()]] on how to specify this parameter.
resurtm committed
241
	 * @param array $params the parameters (name => value) to be bound to the query.
Qiang Xue committed
242
	 * @return integer the number of rows deleted
Qiang Xue committed
243
	 */
Qiang Xue committed
244
	public static function deleteAll($condition = '', $params = array())
w  
Qiang Xue committed
245
	{
Qiang Xue committed
246
		$command = static::getDb()->createCommand();
Qiang Xue committed
247 248
		$command->delete(static::tableName(), $condition, $params);
		return $command->execute();
w  
Qiang Xue committed
249 250
	}

.  
Qiang Xue committed
251
	/**
Qiang Xue committed
252 253 254 255
	 * 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
256
	 * @return ActiveQuery the newly created [[ActiveQuery]] instance.
.  
Qiang Xue committed
257
	 */
Qiang Xue committed
258
	public static function createQuery()
w  
Qiang Xue committed
259
	{
Qiang Xue committed
260 261 262
		return new ActiveQuery(array(
			'modelClass' => get_called_class(),
		));
w  
Qiang Xue committed
263 264 265
	}

	/**
Qiang Xue committed
266
	 * Declares the name of the database table associated with this AR class.
267
	 * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
Qiang Xue committed
268 269
	 * 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
270 271
	 * @return string the table name
	 */
Qiang Xue committed
272
	public static function tableName()
w  
Qiang Xue committed
273
	{
274
		return 'tbl_' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
w  
Qiang Xue committed
275 276 277
	}

	/**
Qiang Xue committed
278 279
	 * 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.
280
	 * @throws InvalidConfigException if the table for the AR class does not exist.
w  
Qiang Xue committed
281
	 */
Qiang Xue committed
282
	public static function getTableSchema()
w  
Qiang Xue committed
283
	{
284 285 286 287 288 289
		$schema = static::getDb()->getTableSchema(static::tableName());
		if ($schema !== null) {
			return $schema;
		} else {
			throw new InvalidConfigException("The table does not exist: " . static::tableName());
		}
w  
Qiang Xue committed
290 291 292
	}

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

310
	/**
311
	 * Returns the name of the column that stores the lock version for implementing optimistic locking.
312
	 *
313 314 315 316
	 * 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.
317 318 319 320 321
	 *
	 * Optimized locking is only supported by [[update()]] and [[delete()]].
	 *
	 * To use optimized locking:
	 *
322
	 * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
323
	 *    Override this method to return the name of this column.
324 325 326 327 328 329 330 331 332
	 * 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.
	 */
333
	public function optimisticLock()
334 335 336 337
	{
		return null;
	}

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
	/**
	 * Declares which DB operations should be performed within a transaction in different scenarios.
	 * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
	 * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
	 * By default, these methods are NOT enclosed in a DB transaction.
	 *
	 * In some scenarios, to ensure data consistency, you may want to enclose some or all of them
	 * in transactions. You can do so by overriding this method and returning the operations
	 * that need to be transactional. For example,
	 *
	 * ~~~
	 * return array(
	 *     'admin' => self::OP_INSERT,
	 *     'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
	 *     // the above is equivalent to the following:
	 *     // 'api' => self::OP_ALL,
	 *
	 * );
	 * ~~~
	 *
	 * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
	 * should be done in a transaction; and in the "api" scenario, all the operations should be done
	 * in a transaction.
	 *
	 * @return array the declarations of transactional operations. The array keys are scenarios names,
	 * and the array values are the corresponding transaction operations.
	 */
	public function transactions()
	{
		return array();
	}

w  
Qiang Xue committed
370
	/**
Qiang Xue committed
371
	 * PHP getter magic method.
Qiang Xue committed
372
	 * This method is overridden so that attributes and related objects can be accessed like properties.
Qiang Xue committed
373 374 375 376 377 378
	 * @param string $name property name
	 * @return mixed property value
	 * @see getAttribute
	 */
	public function __get($name)
	{
Qiang Xue committed
379
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
Qiang Xue committed
380
			return $this->_attributes[$name];
Qiang Xue committed
381
		} elseif (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
382
			return null;
Qiang Xue committed
383 384 385 386 387 388 389 390
		} 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
391
			} else {
Qiang Xue committed
392
				return $value;
Qiang Xue committed
393
			}
Qiang Xue committed
394 395 396 397 398 399 400 401 402 403 404
		}
	}

	/**
	 * 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)
	{
405
		if ($this->hasAttribute($name)) {
Qiang Xue committed
406 407 408 409 410 411 412 413
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
Qiang Xue committed
414
	 * This method overrides the parent implementation by checking if the named attribute is null or not.
Qiang Xue committed
415 416 417 418
	 * @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
419
	{
Qiang Xue committed
420 421 422 423
		try {
			return $this->__get($name) !== null;
		} catch (\Exception $e) {
			return false;
Qiang Xue committed
424 425 426 427 428 429 430 431 432 433 434
		}
	}

	/**
	 * 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
435
		if (isset($this->getTableSchema()->columns[$name])) {
Qiang Xue committed
436
			unset($this->_attributes[$name]);
Qiang Xue committed
437
		} else {
Qiang Xue committed
438 439 440 441 442 443
			$t = strtolower($name);
			if (isset($this->_related[$t])) {
				unset($this->_related[$t]);
			} else {
				parent::__unset($name);
			}
Qiang Xue committed
444 445 446
		}
	}

Qiang Xue committed
447 448 449 450
	/**
	 * 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
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
	 *
	 * 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
471 472 473 474 475 476
	 * @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
477
	public function hasOne($class, $link)
Qiang Xue committed
478
	{
Qiang Xue committed
479
		return new ActiveRelation(array(
480
			'modelClass' => $this->getNamespacedClass($class),
Qiang Xue committed
481 482 483 484
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => false,
		));
Qiang Xue committed
485 486
	}

Qiang Xue committed
487 488 489 490
	/**
	 * 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
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
	 *
	 * 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
509 510 511 512 513 514
	 * @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
515
	public function hasMany($class, $link)
Qiang Xue committed
516
	{
Qiang Xue committed
517
		return new ActiveRelation(array(
518
			'modelClass' => $this->getNamespacedClass($class),
Qiang Xue committed
519 520 521 522
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => true,
		));
Qiang Xue committed
523 524
	}

Qiang Xue committed
525
	/**
Qiang Xue committed
526 527 528 529
	 * 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
530
	 */
Qiang Xue committed
531
	public function populateRelation($name, $records)
Qiang Xue committed
532
	{
Qiang Xue committed
533
		$this->_related[strtolower($name)] = $records;
Qiang Xue committed
534 535
	}

Qiang Xue committed
536 537
	/**
	 * Returns the list of all attribute names of the model.
Qiang Xue committed
538
	 * The default implementation will return all column names of the table associated with this AR class.
Qiang Xue committed
539 540
	 * @return array list of attribute names.
	 */
541
	public function attributes()
Qiang Xue committed
542
	{
Qiang Xue committed
543
		return array_keys($this->getTableSchema()->columns);
544 545
	}

w  
Qiang Xue committed
546 547 548 549 550 551 552 553 554 555
	/**
	 * 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
556
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
w  
Qiang Xue committed
557 558 559 560 561 562
	}

	/**
	 * Sets the named attribute value.
	 * @param string $name the attribute name
	 * @param mixed $value the attribute value.
563
	 * @throws InvalidParamException if the named attribute does not exist.
w  
Qiang Xue committed
564 565 566 567
	 * @see hasAttribute
	 */
	public function setAttribute($name, $value)
	{
568
		if ($this->hasAttribute($name)) {
569 570 571 572
			$this->_attributes[$name] = $value;
		} else {
			throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
		}
w  
Qiang Xue committed
573 574
	}

575 576 577 578 579 580 581 582 583 584
	/**
	 * Returns a value indicating whether the model has an attribute with the specified name.
	 * @param string $name the name of the attribute
	 * @return boolean whether the model has an attribute with the specified name.
	 */
	public function hasAttribute($name)
	{
		return isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name]);
	}

Qiang Xue committed
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
	/**
	 * 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
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
	/**
	 * 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.
622
	 * @throws InvalidParamException if the named attribute does not exist.
Qiang Xue committed
623 624 625 626
	 * @see hasAttribute
	 */
	public function setOldAttribute($name, $value)
	{
627 628 629 630 631
		if (isset($this->_oldAttributes[$name]) || isset($this->getTableSchema()->columns[$name])) {
			$this->_oldAttributes[$name] = $value;
		} else {
			throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
		}
Qiang Xue committed
632 633 634 635 636 637 638 639 640
	}

	/**
	 * 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)
	{
641 642
		if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
			return $this->_attributes[$name] !== $this->_oldAttributes[$name];
Qiang Xue committed
643
		} else {
Qiang Xue committed
644
			return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
Qiang Xue committed
645 646 647
		}
	}

Qiang Xue committed
648 649 650 651 652 653
	/**
	 * 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
654
	public function getDirtyAttributes($names = null)
Qiang Xue committed
655 656
	{
		if ($names === null) {
657
			$names = $this->attributes();
Qiang Xue committed
658 659 660
		}
		$names = array_flip($names);
		$attributes = array();
Qiang Xue committed
661
		if ($this->_oldAttributes === null) {
Qiang Xue committed
662 663 664 665 666 667 668 669 670 671
			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
672
			}
Qiang Xue committed
673
		}
Qiang Xue committed
674
		return $attributes;
w  
Qiang Xue committed
675 676 677 678 679
	}

	/**
	 * Saves the current record.
	 *
Qiang Xue committed
680 681 682 683
	 * 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
684
	 *
Qiang Xue committed
685 686 687 688 689 690
	 * ~~~
	 * $customer = new Customer;  // or $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->save();
	 * ~~~
w  
Qiang Xue committed
691 692 693 694 695 696 697 698 699 700
	 *
	 *
	 * @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)
	{
701 702 703 704 705
		if ($this->getIsNewRecord()) {
			return $this->insert($runValidation, $attributes);
		} else {
			return $this->update($runValidation, $attributes) !== false;
		}
Qiang Xue committed
706 707 708
	}

	/**
Qiang Xue committed
709 710 711 712 713 714
	 * 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;
715 716
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
717
	 *    rest of the steps;
718 719
	 * 4. insert the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
720
	 *
721
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
722 723
	 * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
724 725 726 727
	 *
	 * 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
728
	 * it will be populated with the actual value after insertion.
Qiang Xue committed
729 730 731 732 733 734 735 736 737 738 739 740
	 *
	 * 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
741 742 743
	 * @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.
744
	 * @throws \Exception in case insert failed.
Qiang Xue committed
745
	 */
Qiang Xue committed
746
	public function insert($runValidation = true, $attributes = null)
Qiang Xue committed
747
	{
748 749 750 751
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
		$db = static::getDb();
Qiang Xue committed
752 753 754 755
		if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) {
			$transaction = $db->beginTransaction();
			try {
				$result = $this->insertInternal($attributes);
resurtm committed
756
				if ($result === false) {
757 758 759 760
					$transaction->rollback();
				} else {
					$transaction->commit();
				}
Qiang Xue committed
761
			} catch (\Exception $e) {
762
				$transaction->rollback();
Qiang Xue committed
763
				throw $e;
764
			}
Qiang Xue committed
765 766
		} else {
			$result = $this->insertInternal($attributes);
767 768 769 770 771 772 773
		}
		return $result;
	}

	/**
	 * @see ActiveRecord::insert()
	 */
resurtm committed
774
	private function insertInternal($attributes = null)
775 776
	{
		if (!$this->beforeSave(true)) {
Qiang Xue committed
777 778
			return false;
		}
779 780 781 782
		$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
783
			}
784 785 786
		}
		$db = static::getDb();
		$command = $db->createCommand()->insert($this->tableName(), $values);
787 788 789 790 791 792 793 794 795
		if (!$command->execute()) {
			return false;
		}
		$table = $this->getTableSchema();
		if ($table->sequenceName !== null) {
			foreach ($table->primaryKey as $name) {
				if (!isset($this->_attributes[$name])) {
					$this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName);
					break;
Qiang Xue committed
796 797 798
				}
			}
		}
799 800 801 802 803
		foreach ($values as $name => $value) {
			$this->_oldAttributes[$name] = $value;
		}
		$this->afterSave(true);
		return true;
Qiang Xue committed
804 805 806
	}

	/**
Qiang Xue committed
807 808 809 810 811 812
	 * 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;
813 814
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
Qiang Xue committed
815
	 *    rest of the steps;
816 817
	 * 4. save the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
Qiang Xue committed
818
	 *
819
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
Qiang Xue committed
820 821
	 * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
Qiang Xue committed
822 823 824 825 826 827 828 829 830 831 832 833
	 *
	 * 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();
	 * ~~~
	 *
834 835 836 837 838 839 840 841 842 843 844 845
	 * 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
846 847
	 * @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
848 849
	 * @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.
850 851
	 * @return integer|boolean the number of rows affected, or false if validation fails
	 * or [[beforeSave()]] stops the updating process.
852
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
853
	 * being updated is outdated.
854
	 * @throws \Exception in case update failed.
Qiang Xue committed
855
	 */
Qiang Xue committed
856
	public function update($runValidation = true, $attributes = null)
Qiang Xue committed
857
	{
858
		if ($runValidation && !$this->validate($attributes)) {
Qiang Xue committed
859 860
			return false;
		}
861
		$db = static::getDb();
Qiang Xue committed
862 863 864 865
		if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) {
			$transaction = $db->beginTransaction();
			try {
				$result = $this->updateInternal($attributes);
resurtm committed
866
				if ($result === false) {
867 868 869
					$transaction->rollback();
				} else {
					$transaction->commit();
870
				}
Qiang Xue committed
871
			} catch (\Exception $e) {
872
				$transaction->rollback();
Qiang Xue committed
873
				throw $e;
874
			}
Qiang Xue committed
875 876
		} else {
			$result = $this->updateInternal($attributes);
877 878 879
		}
		return $result;
	}
880

881 882 883 884
	/**
	 * @see CActiveRecord::update()
	 * @throws StaleObjectException
	 */
resurtm committed
885
	private function updateInternal($attributes = null)
886 887 888 889 890 891
	{
		if (!$this->beforeSave(false)) {
			return false;
		}
		$values = $this->getDirtyAttributes($attributes);
		if (empty($values)) {
Qiang Xue committed
892
			$this->afterSave(false);
893 894 895 896 897 898 899
			return 0;
		}
		$condition = $this->getOldPrimaryKey(true);
		$lock = $this->optimisticLock();
		if ($lock !== null) {
			if (!isset($values[$lock])) {
				$values[$lock] = $this->$lock + 1;
Qiang Xue committed
900
			}
901 902 903 904 905
			$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);
906

907 908 909 910 911 912
		if ($lock !== null && !$rows) {
			throw new StaleObjectException('The object being updated is outdated.');
		}

		foreach ($values as $name => $value) {
			$this->_oldAttributes[$name] = $this->_attributes[$name];
Qiang Xue committed
913
		}
914 915
		$this->afterSave(false);
		return $rows;
Qiang Xue committed
916 917 918
	}

	/**
Qiang Xue committed
919
	 * Updates one or several counter columns for the current AR object.
Qiang Xue committed
920 921 922 923 924 925
	 * 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
926
	 * $post = Post::find($id);
Qiang Xue committed
927 928 929 930
	 * $post->updateCounters(array('view_count' => 1));
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value)
Qiang Xue committed
931
	 * Use negative values if you want to decrement the counters.
Qiang Xue committed
932 933 934 935 936
	 * @return boolean whether the saving is successful
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
Qiang Xue committed
937 938 939 940 941 942 943 944
		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
945 946 947 948
		}
	}

	/**
Qiang Xue committed
949 950 951 952 953 954 955 956 957
	 * 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
958
	 * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
Qiang Xue committed
959 960
	 * will be raised by the corresponding methods.
	 *
961 962
	 * @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.
963
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
964
	 * being deleted is outdated.
965
	 * @throws \Exception in case delete failed.
Qiang Xue committed
966 967 968
	 */
	public function delete()
	{
969
		$db = static::getDb();
970
		$transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
971 972 973 974 975 976 977
		try {
			$result = false;
			if ($this->beforeDelete()) {
				// we do not check the return value of deleteAll() because it's possible
				// the record is already deleted in the database and thus the method will return 0
				$condition = $this->getOldPrimaryKey(true);
				$lock = $this->optimisticLock();
resurtm committed
978
				if ($lock !== null) {
979 980 981
					$condition[$lock] = $this->$lock;
				}
				$result = $this->deleteAll($condition);
resurtm committed
982
				if ($lock !== null && !$result) {
983 984 985 986
					throw new StaleObjectException('The object being deleted is outdated.');
				}
				$this->_oldAttributes = null;
				$this->afterDelete();
987
			}
resurtm committed
988 989
			if ($transaction !== null) {
				if ($result === false) {
990 991 992 993
					$transaction->rollback();
				} else {
					$transaction->commit();
				}
994
			}
995
		} catch (\Exception $e) {
resurtm committed
996
			if ($transaction !== null) {
997 998 999
				$transaction->rollback();
			}
			throw $e;
Qiang Xue committed
1000
		}
1001
		return $result;
w  
Qiang Xue committed
1002 1003 1004
	}

	/**
Qiang Xue committed
1005
	 * Returns a value indicating whether the current record is new.
Qiang Xue committed
1006
	 * @return boolean whether the record is new and should be inserted when calling [[save()]].
w  
Qiang Xue committed
1007 1008 1009
	 */
	public function getIsNewRecord()
	{
Qiang Xue committed
1010
		return $this->_oldAttributes === null;
w  
Qiang Xue committed
1011 1012
	}

Qiang Xue committed
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
	/**
	 * Sets the value indicating whether the record is new.
	 * @param boolean $value whether the record is new and should be inserted when calling [[save()]].
	 * @see getIsNewRecord
	 */
	public function setIsNewRecord($value)
	{
		$this->_oldAttributes = $value ? null : $this->_attributes;
	}

1023 1024 1025
	/**
	 * Initializes the object.
	 * This method is called at the end of the constructor.
Qiang Xue committed
1026
	 * The default implementation will trigger an [[EVENT_INIT]] event.
1027 1028 1029 1030 1031 1032
	 * 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();
1033
		$this->trigger(self::EVENT_INIT);
1034 1035 1036 1037
	}

	/**
	 * This method is called when the AR object is created and populated with the query result.
Qiang Xue committed
1038
	 * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
1039 1040 1041 1042 1043
	 * When overriding this method, make sure you call the parent implementation to ensure the
	 * event is triggered.
	 */
	public function afterFind()
	{
1044
		$this->trigger(self::EVENT_AFTER_FIND);
1045 1046
	}

Qiang Xue committed
1047 1048
	/**
	 * This method is called at the beginning of inserting or updating a record.
Qiang Xue committed
1049 1050
	 * 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
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
	 * 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
1070
	public function beforeSave($insert)
w  
Qiang Xue committed
1071
	{
Qiang Xue committed
1072
		$event = new ModelEvent;
1073
		$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
Qiang Xue committed
1074
		return $event->isValid;
w  
Qiang Xue committed
1075 1076
	}

Qiang Xue committed
1077 1078
	/**
	 * This method is called at the end of inserting or updating a record.
Qiang Xue committed
1079 1080
	 * 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
1081 1082 1083 1084 1085
	 * 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
1086
	public function afterSave($insert)
w  
Qiang Xue committed
1087
	{
1088
		$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE);
w  
Qiang Xue committed
1089 1090 1091 1092
	}

	/**
	 * This method is invoked before deleting a record.
Qiang Xue committed
1093
	 * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
Qiang Xue committed
1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
	 * 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
1108 1109
	 * @return boolean whether the record should be deleted. Defaults to true.
	 */
Qiang Xue committed
1110
	public function beforeDelete()
w  
Qiang Xue committed
1111
	{
Qiang Xue committed
1112
		$event = new ModelEvent;
1113
		$this->trigger(self::EVENT_BEFORE_DELETE, $event);
Qiang Xue committed
1114
		return $event->isValid;
w  
Qiang Xue committed
1115 1116 1117 1118
	}

	/**
	 * This method is invoked after deleting a record.
Qiang Xue committed
1119
	 * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
w  
Qiang Xue committed
1120 1121 1122
	 * 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
1123
	public function afterDelete()
w  
Qiang Xue committed
1124
	{
1125
		$this->trigger(self::EVENT_AFTER_DELETE);
w  
Qiang Xue committed
1126 1127 1128
	}

	/**
Qiang Xue committed
1129
	 * Repopulates this active record with the latest data.
Qiang Xue committed
1130
	 * @param array $attributes
Qiang Xue committed
1131 1132
	 * @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
1133
	 */
Qiang Xue committed
1134
	public function refresh($attributes = null)
w  
Qiang Xue committed
1135
	{
Qiang Xue committed
1136
		$record = $this->find($this->getPrimaryKey(true));
Qiang Xue committed
1137 1138 1139 1140
		if ($record === null) {
			return false;
		}
		if ($attributes === null) {
1141
			foreach ($this->attributes() as $name) {
Qiang Xue committed
1142
				$this->_attributes[$name] = $record->_attributes[$name];
Qiang Xue committed
1143
			}
Qiang Xue committed
1144
			$this->_oldAttributes = $this->_attributes;
Qiang Xue committed
1145
		} else {
Qiang Xue committed
1146 1147 1148
			foreach ($attributes as $name) {
				$this->_oldAttributes[$name] = $this->_attributes[$name] = $record->_attributes[$name];
			}
w  
Qiang Xue committed
1149
		}
Qiang Xue committed
1150
		return true;
w  
Qiang Xue committed
1151 1152 1153
	}

	/**
Qiang Xue committed
1154 1155
	 * 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
1156
	 * @param ActiveRecord $record record to compare to
Qiang Xue committed
1157
	 * @return boolean whether the two active records refer to the same row in the same database table.
w  
Qiang Xue committed
1158
	 */
Qiang Xue committed
1159
	public function equals($record)
w  
Qiang Xue committed
1160
	{
Qiang Xue committed
1161
		return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
w  
Qiang Xue committed
1162 1163 1164
	}

	/**
Qiang Xue committed
1165
	 * Returns the primary key value(s).
Qiang Xue committed
1166
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
Qiang Xue committed
1167
	 * the return value will be an array with column names as keys and column values as values.
1168
	 * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
resurtm committed
1169
	 * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1170 1171
	 * 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
1172
	 */
Qiang Xue committed
1173
	public function getPrimaryKey($asArray = false)
w  
Qiang Xue committed
1174
	{
Qiang Xue committed
1175 1176 1177
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
Qiang Xue committed
1178
		} else {
Qiang Xue committed
1179
			$values = array();
Qiang Xue committed
1180
			foreach ($keys as $name) {
Qiang Xue committed
1181
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
Qiang Xue committed
1182 1183
			}
			return $values;
w  
Qiang Xue committed
1184 1185 1186 1187
		}
	}

	/**
Qiang Xue committed
1188
	 * Returns the old primary key value(s).
Qiang Xue committed
1189 1190 1191
	 * 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
1192 1193
	 * @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
1194
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
resurtm committed
1195
	 * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
Qiang Xue committed
1196 1197
	 * 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
1198
	 */
Qiang Xue committed
1199
	public function getOldPrimaryKey($asArray = false)
w  
Qiang Xue committed
1200
	{
Qiang Xue committed
1201 1202 1203
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
Qiang Xue committed
1204 1205
		} else {
			$values = array();
Qiang Xue committed
1206
			foreach ($keys as $name) {
Qiang Xue committed
1207 1208 1209 1210
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
w  
Qiang Xue committed
1211 1212 1213
	}

	/**
Qiang Xue committed
1214
	 * Creates an active record object using a row of data.
Qiang Xue committed
1215
	 * This method is called by [[ActiveQuery]] to populate the query results
1216
	 * into Active Records. It is not meant to be used to create new records.
Qiang Xue committed
1217 1218
	 * @param array $row attribute values (name => value)
	 * @return ActiveRecord the newly created active record.
w  
Qiang Xue committed
1219
	 */
Qiang Xue committed
1220
	public static function create($row)
w  
Qiang Xue committed
1221
	{
Qiang Xue committed
1222
		$record = static::instantiate($row);
1223
		$columns = static::getTableSchema()->columns;
Qiang Xue committed
1224
		foreach ($row as $name => $value) {
Qiang Xue committed
1225
			if (isset($columns[$name])) {
Qiang Xue committed
1226
				$record->_attributes[$name] = $value;
Qiang Xue committed
1227
			} else {
Qiang Xue committed
1228
				$record->$name = $value;
w  
Qiang Xue committed
1229 1230
			}
		}
Qiang Xue committed
1231
		$record->_oldAttributes = $record->_attributes;
1232
		$record->afterFind();
Qiang Xue committed
1233
		return $record;
w  
Qiang Xue committed
1234 1235 1236 1237
	}

	/**
	 * Creates an active record instance.
Qiang Xue committed
1238
	 * This method is called by [[create()]].
w  
Qiang Xue committed
1239
	 * You may override this method if the instance being created
Qiang Xue committed
1240
	 * depends on the row data to be populated into the record.
w  
Qiang Xue committed
1241 1242
	 * 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
1243 1244
	 * @param array $row row data to be populated into the record.
	 * @return ActiveRecord the newly created active record
w  
Qiang Xue committed
1245
	 */
Qiang Xue committed
1246
	public static function instantiate($row)
w  
Qiang Xue committed
1247
	{
Qiang Xue committed
1248
		return new static;
w  
Qiang Xue committed
1249 1250 1251 1252 1253 1254
	}

	/**
	 * 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
1255
	 * @return boolean whether there is an element at the specified offset.
w  
Qiang Xue committed
1256 1257 1258 1259 1260
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}
Qiang Xue committed
1261

Qiang Xue committed
1262
	/**
Qiang Xue committed
1263 1264 1265 1266 1267
	 * 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
1268
	 * @throws InvalidParamException if the named relation does not exist.
Qiang Xue committed
1269 1270 1271 1272 1273 1274 1275 1276 1277
	 */
	public function getRelation($name)
	{
		$getter = 'get' . $name;
		try {
			$relation = $this->$getter();
			if ($relation instanceof ActiveRelation) {
				return $relation;
			}
Qiang Xue committed
1278
		} catch (UnknownMethodException $e) {
1279
			throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
Qiang Xue committed
1280 1281 1282
		}
	}

Qiang Xue committed
1283
	/**
Qiang Xue committed
1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299
	 * 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
1300
	 * @throws InvalidCallException if the method is unable to link two models.
Qiang Xue committed
1301
	 */
Qiang Xue committed
1302
	public function link($name, $model, $extraColumns = array())
Qiang Xue committed
1303
	{
1304 1305 1306
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
Qiang Xue committed
1307 1308 1309
			if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
				throw new InvalidCallException('Unable to link models: both models must NOT be newly created.');
			}
1310
			if (is_array($relation->via)) {
Qiang Xue committed
1311 1312
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1313
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1314
				$viaClass = $viaRelation->modelClass;
1315
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1316
				// unset $viaName so that it can be reloaded to reflect the change
Qiang Xue committed
1317
				unset($this->_related[strtolower($viaName)]);
1318
			} else {
Qiang Xue committed
1319
				$viaRelation = $relation->via;
1320 1321 1322
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1323
			foreach ($viaRelation->link as $a => $b) {
1324 1325 1326 1327 1328
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1329
			foreach ($extraColumns as $k => $v) {
1330 1331
				$columns[$k] = $v;
			}
Qiang Xue committed
1332
			static::getDb()->createCommand()
Qiang Xue committed
1333 1334 1335 1336 1337 1338
				->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
1339
					throw new InvalidCallException('Unable to link models: both models are newly created.');
Qiang Xue committed
1340 1341
				} elseif ($this->getIsNewRecord()) {
					$this->bindModels(array_flip($relation->link), $this, $model);
Qiang Xue committed
1342
				} else {
Qiang Xue committed
1343
					$this->bindModels($relation->link, $model, $this);
1344
				}
Qiang Xue committed
1345 1346 1347 1348
			} elseif ($p1) {
				$this->bindModels(array_flip($relation->link), $this, $model);
			} elseif ($p2) {
				$this->bindModels($relation->link, $model, $this);
1349
			} else {
Qiang Xue committed
1350
				throw new InvalidCallException('Unable to link models: the link does not involve any primary key.');
1351 1352
			}
		}
Qiang Xue committed
1353

Qiang Xue committed
1354
		// update lazily loaded related objects
Qiang Xue committed
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364
		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;
			}
		}
1365 1366 1367
	}

	/**
Qiang Xue committed
1368 1369 1370 1371 1372 1373 1374
	 * 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
1375 1376
	 * @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
1377
	 * If true, the model containing the foreign key will be deleted.
Qiang Xue committed
1378
	 * @throws InvalidCallException if the models cannot be unlinked
1379
	 */
Qiang Xue committed
1380
	public function unlink($name, $model, $delete = false)
1381 1382 1383 1384 1385
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
Qiang Xue committed
1386 1387
				/** @var $viaRelation ActiveRelation */
				list($viaName, $viaRelation) = $relation->via;
1388
				/** @var $viaClass ActiveRecord */
Qiang Xue committed
1389
				$viaClass = $viaRelation->modelClass;
1390
				$viaTable = $viaClass::tableName();
Qiang Xue committed
1391
				unset($this->_related[strtolower($viaName)]);
1392
			} else {
Qiang Xue committed
1393
				$viaRelation = $relation->via;
1394 1395 1396
				$viaTable = reset($relation->via->from);
			}
			$columns = array();
Qiang Xue committed
1397
			foreach ($viaRelation->link as $a => $b) {
1398 1399 1400 1401 1402
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
Qiang Xue committed
1403
			$command = static::getDb()->createCommand();
Qiang Xue committed
1404 1405 1406 1407 1408 1409 1410 1411
			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();
1412 1413
			}
		} else {
Qiang Xue committed
1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426
			$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
1427
				throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
Qiang Xue committed
1428
			}
1429
		}
Qiang Xue committed
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440

		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]);
				}
			}
		}
1441 1442
	}

Qiang Xue committed
1443 1444 1445 1446 1447 1448 1449 1450
	/**
	 * 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
	 */
1451
	protected static function getNamespacedClass($class)
Qiang Xue committed
1452 1453
	{
		if (strpos($class, '\\') === false) {
1454
			$reflector = new \ReflectionClass(static::className());
1455 1456 1457
			return $reflector->getNamespaceName() . '\\' . $class;
		} else {
			return $class;
Qiang Xue committed
1458 1459 1460
		}
	}

1461
	/**
Qiang Xue committed
1462 1463 1464
	 * @param array $link
	 * @param ActiveRecord $foreignModel
	 * @param ActiveRecord $primaryModel
Qiang Xue committed
1465
	 * @throws InvalidCallException
1466
	 */
Qiang Xue committed
1467
	private function bindModels($link, $foreignModel, $primaryModel)
1468
	{
Qiang Xue committed
1469 1470 1471
		foreach ($link as $fk => $pk) {
			$value = $primaryModel->$pk;
			if ($value === null) {
Qiang Xue committed
1472
				throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
Qiang Xue committed
1473
			}
Qiang Xue committed
1474
			$foreignModel->$fk = $value;
Qiang Xue committed
1475
		}
Qiang Xue committed
1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491
		$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
1492
	}
1493 1494

	/**
1495 1496 1497
	 * Returns a value indicating whether the specified operation is transactional in the current [[scenario]].
	 * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
	 * @return boolean whether the specified operation is transactional in the current [[scenario]].
1498
	 */
1499
	public function isTransactional($operation)
1500 1501
	{
		$scenario = $this->getScenario();
1502 1503
		$transactions = $this->transactions();
		return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
1504
	}
w  
Qiang Xue committed
1505
}