BaseActiveRecord.php 47.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php
/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\db;

use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\base\InvalidParamException;
use yii\base\ModelEvent;
use yii\base\NotSupportedException;
use yii\base\UnknownMethodException;
use yii\base\InvalidCallException;

/**
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
 *
22
 * See [[\yii\db\ActiveRecord]] for a concrete implementation.
23 24 25 26 27 28 29 30 31 32 33
 *
 * @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. 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. A string is returned otherwise (null will be returned if the key value is null).
 * This property is read-only.
Qiang Xue committed
34 35
 * @property array $relatedRecords An array of related records indexed by relation names. This property is
 * read-only.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{
	/**
	 * @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';

	/**
	 * @var array attribute values indexed by attribute names
	 */
	private $_attributes = [];
	/**
	 * @var array old attribute values indexed by attribute names.
	 */
	private $_oldAttributes;
	/**
	 * @var array related models indexed by the relation names
	 */
	private $_related = [];


	/**
	 * Creates an [[ActiveQuery]] instance for query purpose.
	 *
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
	 * The returned [[ActiveQuery]] instance can be further customized by calling
	 * methods defined in [[ActiveQuery]] before `one()`, `all()` or `value()` is
	 * called to return the populated active records:
	 *
	 * ~~~
	 * // find all customers
	 * $customers = Customer::find()->all();
	 *
	 * // find all active customers and order them by their age:
	 * $customers = Customer::find()
	 * ->where(['status' => 1])
	 * ->orderBy('age')
	 * ->all();
	 *
	 * // find a single customer whose primary key value is 10
	 * $customer = Customer::find(10);
	 *
	 * // the above is equivalent to:
	 * $customer = Customer::find()->where(['id' => 10])->one();
	 *
	 * // find a single customer whose age is 30 and whose status is 1
	 * $customer = Customer::find(['age' => 30, 'status' => 1]);
	 *
	 * // the above is equivalent to:
	 * $customer = Customer::find()->where(['age' => 30, 'status' => 1])->one();
	 * ~~~
122 123 124 125 126 127 128 129
	 *
	 * @param mixed $q the query parameter. This can be one of the followings:
	 *
	 *  - a scalar value (integer or string): query by a single primary key value and return the
	 *    corresponding record.
	 *  - an array of name-value pairs: query by a set of column values and return a single record matching all of them.
	 *  - null: return a new [[ActiveQuery]] object for further query purpose.
	 *
Alexander Makarov committed
130
	 * @return ActiveQuery|static|null When `$q` is null, a new [[ActiveQuery]] instance
131 132 133 134 135 136 137 138 139
	 * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be
	 * returned (null will be returned if there is no matching).
	 * @throws InvalidConfigException if the AR class does not have a primary key
	 * @see createQuery()
	 */
	public static function find($q = null)
	{
		$query = static::createQuery();
		if (is_array($q)) {
140
			return $query->andWhere($q)->one();
141 142 143 144
		} elseif ($q !== null) {
			// query by primary key
			$primaryKey = static::primaryKey();
			if (isset($primaryKey[0])) {
145
				return $query->andWhere([$primaryKey[0] => $q])->one();
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
			} else {
				throw new InvalidConfigException(get_called_class() . ' must have a primary key.');
			}
		}
		return $query;
	}

	/**
	 * Updates the whole table using the provided attribute values and conditions.
	 * For example, to change the status to be 1 for all customers whose status is 2:
	 *
	 * ~~~
	 * Customer::updateAll(['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.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @return integer the number of rows updated
	 */
	public static function updateAll($attributes, $condition = '')
	{
		throw new NotSupportedException(__METHOD__ . ' is not supported.');
	}

	/**
	 * Updates the whole table using the provided counter changes and conditions.
	 * For example, to increment all customers' age by 1,
	 *
	 * ~~~
	 * Customer::updateAllCounters(['age' => 1]);
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value).
	 * 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.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @return integer the number of rows updated
	 */
	public static function updateAllCounters($counters, $condition = '')
	{
		throw new NotSupportedException(__METHOD__ . ' is not supported.');
	}

	/**
	 * Deletes rows in the table using the provided conditions.
	 * 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.
	 * Please refer to [[Query::where()]] on how to specify this parameter.
	 * @param array $params the parameters (name => value) to be bound to the query.
	 * @return integer the number of rows deleted
	 */
	public static function deleteAll($condition = '', $params = [])
	{
		throw new NotSupportedException(__METHOD__ . ' is not supported.');
	}

	/**
	 * Returns the name of the column that stores the lock version for implementing optimistic locking.
	 *
	 * 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.
	 *
	 * Optimistic locking is only supported by [[update()]] and [[delete()]].
	 *
	 * To use Optimistic locking:
	 *
	 * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
	 *    Override this method to return the name of this column.
	 * 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.
	 */
	public function optimisticLock()
	{
		return null;
	}

	/**
	 * PHP getter magic method.
	 * This method is overridden so that attributes and related objects can be accessed like properties.
241
	 *
242
	 * @param string $name property name
243
	 * @throws \yii\base\InvalidParamException if relation name is wrong
244 245 246 247 248 249 250 251 252 253 254 255 256 257
	 * @return mixed property value
	 * @see getAttribute()
	 */
	public function __get($name)
	{
		if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
			return $this->_attributes[$name];
		} elseif ($this->hasAttribute($name)) {
			return null;
		} else {
			if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
				return $this->_related[$name];
			}
			$value = parent::__get($name);
258
			if ($value instanceof ActiveQueryInterface) {
259 260 261
				return $this->_related[$name] = $value->findFor($name, $this);
			} else {
				return $value;
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
			}
		}
	}

	/**
	 * 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)
	{
		if ($this->hasAttribute($name)) {
			$this->_attributes[$name] = $value;
		} else {
			parent::__set($name, $value);
		}
	}

	/**
	 * Checks if a property value is null.
	 * This method overrides the parent implementation by checking if the named attribute is null or not.
	 * @param string $name the property name or the event name
	 * @return boolean whether the property value is null
	 */
	public function __isset($name)
	{
		try {
			return $this->__get($name) !== null;
		} catch (\Exception $e) {
			return false;
		}
	}

	/**
	 * 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)
	{
		if ($this->hasAttribute($name)) {
			unset($this->_attributes[$name]);
306 307
		} elseif (array_key_exists($name, $this->_related)) {
			unset($this->_related[$name]);
308
		} elseif ($this->getRelation($name, false) === null) {
309
			parent::__unset($name);
310 311 312 313 314
		}
	}

	/**
	 * Declares a `has-one` relation.
315
	 * The declaration is returned in terms of a relational [[ActiveQuery]] instance
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
	 * through which the related record can be queried and retrieved back.
	 *
	 * 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::className(), ['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.
	 *
335
	 * Call methods declared in [[ActiveQuery]] to further customize the relation.
336 337 338 339 340
	 *
	 * @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 attributes of the record associated with the `$class` model, while the values of the
	 * array refer to the corresponding attributes in **this** AR class.
341
	 * @return ActiveQueryInterface the relational query object.
342 343 344
	 */
	public function hasOne($class, $link)
	{
345
		/** @var ActiveRecordInterface $class */
346
		return $class::createQuery([
347 348 349 350 351 352 353 354 355
			'modelClass' => $class,
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => false,
		]);
	}

	/**
	 * Declares a `has-many` relation.
356
	 * The declaration is returned in terms of a relational [[ActiveQuery]] instance
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
	 * through which the related record can be queried and retrieved back.
	 *
	 * 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::className(), ['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.
	 *
376 377
	 * Call methods declared in [[ActiveQuery]] to further customize the relation.
	 *
378 379 380 381
	 * @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 attributes of the record associated with the `$class` model, while the values of the
	 * array refer to the corresponding attributes in **this** AR class.
382
	 * @return ActiveQueryInterface the relational query object.
383 384 385
	 */
	public function hasMany($class, $link)
	{
386
		/** @var ActiveRecordInterface $class */
387
		return $class::createQuery([
388 389 390 391 392 393 394 395 396 397 398
			'modelClass' => $class,
			'primaryModel' => $this,
			'link' => $link,
			'multiple' => true,
		]);
	}

	/**
	 * 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-sensitive)
399
	 * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
400 401 402 403 404 405 406 407 408
	 */
	public function populateRelation($name, $records)
	{
		$this->_related[$name] = $records;
	}

	/**
	 * Check whether the named relation has been populated with records.
	 * @param string $name the relation name (case-sensitive)
409
	 * @return boolean whether relation has been populated with records.
410 411 412 413 414 415 416
	 */
	public function isRelationPopulated($name)
	{
		return array_key_exists($name, $this->_related);
	}

	/**
417 418
	 * Returns all populated related records.
	 * @return array an array of related records indexed by relation names.
419
	 */
420
	public function getRelatedRecords()
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
	{
		return $this->_related;
	}

	/**
	 * 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]) || in_array($name, $this->attributes());
	}

	/**
	 * 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)
	{
		return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
	}

	/**
	 * Sets the named attribute value.
	 * @param string $name the attribute name
	 * @param mixed $value the attribute value.
	 * @throws InvalidParamException if the named attribute does not exist.
	 * @see hasAttribute()
	 */
	public function setAttribute($name, $value)
	{
		if ($this->hasAttribute($name)) {
			$this->_attributes[$name] = $value;
		} else {
			throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
		}
	}

	/**
	 * Returns the old attribute values.
	 * @return array the old attribute values (name-value pairs)
	 */
	public function getOldAttributes()
	{
		return $this->_oldAttributes === null ? [] : $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;
	}

	/**
	 * 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.
	 * @throws InvalidParamException if the named attribute does not exist.
	 * @see hasAttribute()
	 */
	public function setOldAttribute($name, $value)
	{
		if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) {
			$this->_oldAttributes[$name] = $value;
		} else {
			throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
		}
	}

513 514 515 516 517 518 519 520 521 522 523
	/**
	 * Marks an attribute dirty.
	 * This method may be called to force updating a record when calling [[update()]],
	 * even if there is no change being made to the record.
	 * @param string $name the attribute name
	 */
	public function markAttributeDirty($name)
	{
		unset($this->_oldAttributes[$name]);
	}

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
	/**
	 * Returns a value indicating whether the named attribute has been changed.
	 * @param string $name the name of the attribute
	 * @return boolean whether the attribute has been changed
	 */
	public function isAttributeChanged($name)
	{
		if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
			return $this->_attributes[$name] !== $this->_oldAttributes[$name];
		} else {
			return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
		}
	}

	/**
	 * 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)
	 */
	public function getDirtyAttributes($names = null)
	{
		if ($names === null) {
			$names = $this->attributes();
		}
		$names = array_flip($names);
		$attributes = [];
		if ($this->_oldAttributes === null) {
			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;
				}
			}
		}
		return $attributes;
	}

	/**
	 * Saves the current record.
	 *
	 * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]]
	 * when [[isNewRecord]] is false.
	 *
	 * For example, to save a customer record:
	 *
	 * ~~~
	 * $customer = new Customer;  // or $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->save();
	 * ~~~
	 *
	 *
	 * @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)
	{
		if ($this->getIsNewRecord()) {
			return $this->insert($runValidation, $attributes);
		} else {
			return $this->update($runValidation, $attributes) !== false;
		}
	}

	/**
	 * 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;
	 * 2. call [[afterValidate()]] when `$runValidation` is true.
	 * 3. call [[beforeSave()]]. If the method returns false, it will skip the
	 *    rest of the steps;
	 * 4. save the record into database. If this fails, it will skip the rest of the steps;
	 * 5. call [[afterSave()]];
	 *
	 * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
	 * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
	 * will be raised by the corresponding methods.
	 *
615
	 * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
	 *
	 * For example, to update a customer record:
	 *
	 * ~~~
	 * $customer = Customer::find($id);
	 * $customer->name = $name;
	 * $customer->email = $email;
	 * $customer->update();
	 * ~~~
	 *
	 * 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
	 * }
	 * ~~~
	 *
	 * @param boolean $runValidation whether to perform validation before saving the record.
	 * If the validation fails, the record will not be inserted into the 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 integer|boolean the number of rows affected, or false if validation fails
	 * or [[beforeSave()]] stops the updating process.
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
	 * being updated is outdated.
	 * @throws \Exception in case update failed.
	 */
	public function update($runValidation = true, $attributes = null)
	{
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
		return $this->updateInternal($attributes);
	}

	/**
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
	 * Updates the specified attributes.
	 *
	 * This method is a shortcut to [[update()]] when data validation is not needed
	 * and only a list of attributes need to be updated.
	 *
	 * You may specify the attributes to be updated as name list or name-value pairs.
	 * If the latter, the corresponding attribute values will be modified accordingly.
	 * The method will then save the specified attributes into database.
	 *
	 * Note that this method will NOT perform data validation.
	 *
	 * @param array $attributes the attributes (names or name-value pairs) to be updated
	 * @return integer|boolean the number of rows affected, or false if [[beforeSave()]] stops the updating process.
	 */
	public function updateAttributes($attributes)
	{
		$attrs = [];
		foreach ($attributes as $name => $value) {
			if (is_integer($name)) {
				$attrs[] = $value;
			} else {
				$this->$name = $value;
				$attrs[] = $name;
			}
		}
		return $this->update(false, $attrs);
	}

	/**
	 * @see update()
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938
	 * @throws StaleObjectException
	 */
	protected function updateInternal($attributes = null)
	{
		if (!$this->beforeSave(false)) {
			return false;
		}
		$values = $this->getDirtyAttributes($attributes);
		if (empty($values)) {
			$this->afterSave(false);
			return 0;
		}
		$condition = $this->getOldPrimaryKey(true);
		$lock = $this->optimisticLock();
		if ($lock !== null) {
			if (!isset($values[$lock])) {
				$values[$lock] = $this->$lock + 1;
			}
			$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);

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

		foreach ($values as $name => $value) {
			$this->_oldAttributes[$name] = $this->_attributes[$name];
		}
		$this->afterSave(false);
		return $rows;
	}

	/**
	 * Updates one or several counter columns for the current AR object.
	 * Note that this method differs from [[updateAllCounters()]] in that it only
	 * saves counters for the current AR object.
	 *
	 * An example usage is as follows:
	 *
	 * ~~~
	 * $post = Post::find($id);
	 * $post->updateCounters(['view_count' => 1]);
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value)
	 * Use negative values if you want to decrement the counters.
	 * @return boolean whether the saving is successful
	 * @see updateAllCounters()
	 */
	public function updateCounters($counters)
	{
		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;
		}
	}

	/**
	 * 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()]].
	 *
	 * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
	 * will be raised by the corresponding methods.
	 *
	 * @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.
	 * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
	 * being deleted is outdated.
	 * @throws \Exception in case delete failed.
	 */
	public function delete()
	{
		$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();
			if ($lock !== null) {
				$condition[$lock] = $this->$lock;
			}
			$result = $this->deleteAll($condition);
			if ($lock !== null && !$result) {
				throw new StaleObjectException('The object being deleted is outdated.');
			}
			$this->_oldAttributes = null;
			$this->afterDelete();
		}
		return $result;
	}

	/**
	 * Returns a value indicating whether the current record is new.
	 * @return boolean whether the record is new and should be inserted when calling [[save()]].
	 */
	public function getIsNewRecord()
	{
		return $this->_oldAttributes === null;
	}

	/**
	 * 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;
	}

	/**
	 * Initializes the object.
	 * This method is called at the end of the constructor.
	 * The default implementation will trigger an [[EVENT_INIT]] event.
	 * 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();
		$this->trigger(self::EVENT_INIT);
	}

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

	/**
	 * This method is called at the beginning of inserting or updating a record.
	 * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true,
	 * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false.
	 * 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.
	 */
	public function beforeSave($insert)
	{
		$event = new ModelEvent;
		$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
		return $event->isValid;
	}

	/**
	 * This method is called at the end of inserting or updating a record.
	 * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true,
	 * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false.
	 * 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.
	 */
	public function afterSave($insert)
	{
		$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE);
	}

	/**
	 * This method is invoked before deleting a record.
	 * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
	 * 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;
	 *     }
	 * }
	 * ~~~
	 *
	 * @return boolean whether the record should be deleted. Defaults to true.
	 */
	public function beforeDelete()
	{
		$event = new ModelEvent;
		$this->trigger(self::EVENT_BEFORE_DELETE, $event);
		return $event->isValid;
	}

	/**
	 * This method is invoked after deleting a record.
	 * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
	 * 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.
	 */
	public function afterDelete()
	{
		$this->trigger(self::EVENT_AFTER_DELETE);
	}

	/**
	 * Repopulates this active record with the latest data.
	 * @return boolean whether the row still exists in the database. If true, the latest data
	 * will be populated to this active record. Otherwise, this record will remain unchanged.
	 */
	public function refresh()
	{
		$record = $this->find($this->getPrimaryKey(true));
		if ($record === null) {
			return false;
		}
		foreach ($this->attributes() as $name) {
			$this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
		}
		$this->_oldAttributes = $this->_attributes;
		$this->_related = [];
		return true;
	}

	/**
	 * 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.
	 * If one of the records [[isNewRecord|is new]] they are also considered not equal.
939
	 * @param ActiveRecordInterface $record record to compare to
940 941 942 943
	 * @return boolean whether the two active records refer to the same row in the same database table.
	 */
	public function equals($record)
	{
944
		if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
			return false;
		}
		return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
	}

	/**
	 * Returns the primary key value(s).
	 * @param boolean $asArray whether to return the primary key value as an array. If true,
	 * the return value will be an array with column names as keys and column values as values.
	 * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
	 * @property mixed The primary key value. An array (column name => column value) is returned if
	 * the primary key is composite. A string is returned otherwise (null will be returned if
	 * the key value is null).
	 * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
	 * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
	 * the key value is null).
	 */
	public function getPrimaryKey($asArray = false)
	{
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
		} else {
			$values = [];
			foreach ($keys as $name) {
				$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
			}
			return $values;
		}
	}

	/**
	 * Returns the old primary key value(s).
	 * 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.
	 * @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.
	 * If this is false (default), a scalar value will be returned for non-composite primary key.
	 * @property mixed The old primary key value. An array (column name => column value) is
	 * returned if the primary key is composite. A string is returned otherwise (null will be
	 * returned if the key value is null).
	 * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
	 * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
	 * the key value is null).
	 */
	public function getOldPrimaryKey($asArray = false)
	{
		$keys = $this->primaryKey();
		if (count($keys) === 1 && !$asArray) {
			return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
		} else {
			$values = [];
			foreach ($keys as $name) {
				$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
			}
			return $values;
		}
	}

	/**
1006
	 * Populates an active record object using a row of data from the database/storage.
Carsten Brandt committed
1007
	 *
1008
	 * This is an internal method meant to be called to create active record objects after
Carsten Brandt committed
1009
	 * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
1010
	 * the query results into active records.
Carsten Brandt committed
1011 1012 1013 1014
	 *
	 * When calling this method manually you should call [[afterFind()]] on the created
	 * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
	 *
1015 1016
	 * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
	 * created by [[instantiate()]] beforehand.
1017 1018
	 * @param array $row attribute values (name => value)
	 */
1019
	public static function populateRecord($record, $row)
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
	{
		$columns = array_flip($record->attributes());
		foreach ($row as $name => $value) {
			if (isset($columns[$name])) {
				$record->_attributes[$name] = $value;
			} else {
				$record->$name = $value;
			}
		}
		$record->_oldAttributes = $record->_attributes;
	}

	/**
	 * Creates an active record instance.
1034 1035
	 *
	 * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
1036
	 * It is not meant to be used for creating new records directly.
1037
	 *
1038 1039 1040 1041 1042
	 * You may override this method if the instance being created
	 * depends on the row data to be populated into the record.
	 * For example, by creating a record based on the value of a column,
	 * you may implement the so-called single-table inheritance mapping.
	 * @param array $row row data to be populated into the record.
Carsten Brandt committed
1043
	 * @return static the newly created active record
1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
	 */
	public static function instantiate($row)
	{
		return new static;
	}

	/**
	 * Returns whether there is an element at the specified offset.
	 * This method is required by the interface ArrayAccess.
	 * @param mixed $offset the offset to check on
	 * @return boolean whether there is an element at the specified offset.
	 */
	public function offsetExists($offset)
	{
		return $this->__isset($offset);
	}

	/**
	 * Returns the relation object with the specified name.
1063
	 * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object.
1064 1065
	 * It can be declared in either the Active Record class itself or one of its behaviors.
	 * @param string $name the relation name
1066 1067 1068
	 * @param boolean $throwException whether to throw exception if the relation does not exist.
	 * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
	 * and `$throwException` is false, null will be returned.
1069 1070
	 * @throws InvalidParamException if the named relation does not exist.
	 */
1071
	public function getRelation($name, $throwException = true)
1072 1073 1074
	{
		$getter = 'get' . $name;
		try {
1075
			// the relation could be defined in a behavior
1076 1077
			$relation = $this->$getter();
		} catch (UnknownMethodException $e) {
1078 1079 1080 1081 1082
			if ($throwException) {
				throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
			} else {
				return null;
			}
1083
		}
1084
		if (!$relation instanceof ActiveQueryInterface) {
1085 1086 1087 1088 1089
			if ($throwException) {
				throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
			} else {
				return null;
			}
1090 1091 1092 1093 1094 1095 1096
		}

		if (method_exists($this, $getter)) {
			// relation name is case sensitive, trying to validate it when the relation is defined within this class
			$method = new \ReflectionMethod($this, $getter);
			$realName = lcfirst(substr($method->getName(), 3));
			if ($realName !== $name) {
1097 1098 1099 1100 1101
				if ($throwException) {
					throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
				} else {
					return null;
				}
1102 1103 1104 1105
			}
		}

		return $relation;
1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
	}

	/**
	 * 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 case sensitive name of the relationship
1121
	 * @param ActiveRecordInterface $model the model to be linked with the current one.
1122 1123
	 * @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
1124
	 * (i.e., a relation set with [[ActiveRelationTrait::via()]] or `[[ActiveQuery::viaTable()]]`.)
1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135
	 * @throws InvalidCallException if the method is unable to link two models.
	 */
	public function link($name, $model, $extraColumns = [])
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
				throw new InvalidCallException('Unable to link models: both models must NOT be newly created.');
			}
			if (is_array($relation->via)) {
1136
				/** @var ActiveQuery $viaRelation */
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155
				list($viaName, $viaRelation) = $relation->via;
				$viaClass = $viaRelation->modelClass;
				// unset $viaName so that it can be reloaded to reflect the change
				unset($this->_related[$viaName]);
			} else {
				$viaRelation = $relation->via;
				$viaTable = reset($relation->via->from);
			}
			$columns = [];
			foreach ($viaRelation->link as $a => $b) {
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
			foreach ($extraColumns as $k => $v) {
				$columns[$k] = $v;
			}
			if (is_array($relation->via)) {
1156 1157
				/** @var $viaClass ActiveRecordInterface */
				/** @var $record ActiveRecordInterface */
1158
				$record = new $viaClass();
1159
				foreach ($columns as $column => $value) {
1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
					$record->$column = $value;
				}
				$record->insert(false);
			} else {
				/** @var $viaTable string */
				static::getDb()->createCommand()
					->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()) {
					throw new InvalidCallException('Unable to link models: both models are newly created.');
				} elseif ($this->getIsNewRecord()) {
					$this->bindModels(array_flip($relation->link), $this, $model);
				} else {
					$this->bindModels($relation->link, $model, $this);
				}
			} elseif ($p1) {
				$this->bindModels(array_flip($relation->link), $this, $model);
			} elseif ($p2) {
				$this->bindModels($relation->link, $model, $this);
			} else {
				throw new InvalidCallException('Unable to link models: the link does not involve any primary key.');
			}
		}

		// update lazily loaded related objects
		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;
			}
		}
	}

	/**
	 * 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 case sensitive name of the relationship.
1208
	 * @param ActiveRecordInterface $model the model to be unlinked from the current one.
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
	 * @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.
	 * If true, the model containing the foreign key will be deleted.
	 * @throws InvalidCallException if the models cannot be unlinked
	 */
	public function unlink($name, $model, $delete = false)
	{
		$relation = $this->getRelation($name);

		if ($relation->via !== null) {
			if (is_array($relation->via)) {
1220
				/** @var ActiveQuery $viaRelation */
1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235
				list($viaName, $viaRelation) = $relation->via;
				$viaClass = $viaRelation->modelClass;
				unset($this->_related[$viaName]);
			} else {
				$viaRelation = $relation->via;
				$viaTable = reset($relation->via->from);
			}
			$columns = [];
			foreach ($viaRelation->link as $a => $b) {
				$columns[$a] = $this->$b;
			}
			foreach ($relation->link as $a => $b) {
				$columns[$b] = $model->$a;
			}
			if (is_array($relation->via)) {
1236
				/** @var $viaClass ActiveRecordInterface */
1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247
				if ($delete) {
					$viaClass::deleteAll($columns);
				} else {
					$nulls = [];
					foreach (array_keys($columns) as $a) {
						$nulls[$a] = null;
					}
					$viaClass::updateAll($nulls, $columns);
				}
			} else {
				/** @var $viaTable string */
1248
				/** @var Command $command */
1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280
				$command = static::getDb()->createCommand();
				if ($delete) {
					$command->delete($viaTable, $columns)->execute();
				} else {
					$nulls = [];
					foreach (array_keys($columns) as $a) {
						$nulls[$a] = null;
					}
					$command->update($viaTable, $nulls, $columns)->execute();
				}
			}
		} else {
			$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 {
				throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
			}
		}

		if (!$relation->multiple) {
			unset($this->_related[$name]);
		} elseif (isset($this->_related[$name])) {
1281
			/** @var ActiveRecordInterface $b */
1282 1283 1284 1285 1286 1287 1288 1289 1290 1291
			foreach ($this->_related[$name] as $a => $b) {
				if ($model->getPrimaryKey() == $b->getPrimaryKey()) {
					unset($this->_related[$name][$a]);
				}
			}
		}
	}

	/**
	 * @param array $link
1292 1293
	 * @param BaseActiveRecord $foreignModel
	 * @param BaseActiveRecord $primaryModel
1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
	 * @throws InvalidCallException
	 */
	private function bindModels($link, $foreignModel, $primaryModel)
	{
		foreach ($link as $fk => $pk) {
			$value = $primaryModel->$pk;
			if ($value === null) {
				throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
			}
			$foreignModel->$fk = $value;
		}
		$foreignModel->save(false);
	}

	/**
	 * Returns a value indicating whether the given set of attributes represents the primary key for this model
	 * @param array $keys the set of attributes to check
	 * @return boolean whether the given set of attributes represents the primary key for this model
	 */
	public static function isPrimaryKey($keys)
	{
		$pks = static::primaryKey();
1316 1317 1318 1319
		if (count($keys) === count($pks)) {
			return count(array_intersect($keys, $pks)) === count($pks);
		} else {
			return false;
1320 1321
		}
	}
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335

	/**
	 * Returns the text label for the specified attribute.
	 * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
	 * @param string $attribute the attribute name
	 * @return string the attribute label
	 * @see generateAttributeLabel()
	 * @see attributeLabels()
	 */
	public function getAttributeLabel($attribute)
	{
		$labels = $this->attributeLabels();
		if (isset($labels[$attribute])) {
			return ($labels[$attribute]);
1336
		} elseif (strpos($attribute, '.')) {
1337 1338 1339 1340 1341
			$attributeParts = explode('.', $attribute);
			$neededAttribute = array_pop($attributeParts);

			$relatedModel = $this;
			foreach ($attributeParts as $relationName) {
1342 1343 1344 1345 1346 1347 1348 1349 1350
				if (isset($this->_related[$relationName]) && $this->_related[$relationName] instanceof self) {
					$relatedModel = $this->_related[$relationName];
				} else {
					try {
						$relation = $relatedModel->getRelation($relationName);
					} catch (InvalidParamException $e) {
						return $this->generateAttributeLabel($attribute);
					}
					$relatedModel = new $relation->modelClass;
1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361
				}
			}

			$labels = $relatedModel->attributeLabels();
			if (isset($labels[$neededAttribute])) {
				return $labels[$neededAttribute];
			}
		}

		return $this->generateAttributeLabel($attribute);
	}
Qiang Xue committed
1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378

	/**
	 * @inheritdoc
	 *
	 * The default implementation returns the names of the columns whose values have been populated into this record.
	 */
	public function fields()
	{
		$fields = array_keys($this->_attributes);
		return array_combine($fields, $fields);
	}

	/**
	 * @inheritdoc
	 *
	 * The default implementation returns the names of the relations that have been populated into this record.
	 */
Qiang Xue committed
1379
	public function extraFields()
Qiang Xue committed
1380 1381 1382 1383
	{
		$fields = array_keys($this->getRelatedRecords());
		return array_combine($fields, $fields);
	}
1384 1385 1386 1387 1388 1389 1390 1391 1392

	/**
	 * Sets the element value at the specified offset to null.
	 * This method is required by the SPL interface `ArrayAccess`.
	 * It is implicitly called when you use something like `unset($model[$offset])`.
	 * @param mixed $offset the offset to unset element
	 */
	public function offsetUnset($offset)
	{
Qiang Xue committed
1393 1394 1395 1396 1397
		if (property_exists($this, $offset)) {
			$this->$offset = null;
		} else {
			unset($this->$offset);
		}
1398
	}
1399
}