You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1605 lines
62KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\db;
  8. use yii\base\InvalidConfigException;
  9. use yii\base\Event;
  10. use yii\base\Model;
  11. use yii\base\InvalidParamException;
  12. use yii\base\ModelEvent;
  13. use yii\base\NotSupportedException;
  14. use yii\base\UnknownMethodException;
  15. use yii\base\InvalidCallException;
  16. use yii\helpers\ArrayHelper;
  17. /**
  18. * ActiveRecord is the base class for classes representing relational data in terms of objects.
  19. *
  20. * See [[\yii\db\ActiveRecord]] for a concrete implementation.
  21. *
  22. * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is
  23. * read-only.
  24. * @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]].
  25. * @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this
  26. * property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details.
  27. * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
  28. * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
  29. * value is null). This property is read-only.
  30. * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if
  31. * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
  32. * This property is read-only.
  33. * @property array $relatedRecords An array of related records indexed by relation names. This property is
  34. * read-only.
  35. *
  36. * @author Qiang Xue <qiang.xue@gmail.com>
  37. * @author Carsten Brandt <mail@cebe.cc>
  38. * @since 2.0
  39. */
  40. abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
  41. {
  42. /**
  43. * @event Event an event that is triggered when the record is initialized via [[init()]].
  44. */
  45. const EVENT_INIT = 'init';
  46. /**
  47. * @event Event an event that is triggered after the record is created and populated with query result.
  48. */
  49. const EVENT_AFTER_FIND = 'afterFind';
  50. /**
  51. * @event ModelEvent an event that is triggered before inserting a record.
  52. * You may set [[ModelEvent::isValid]] to be false to stop the insertion.
  53. */
  54. const EVENT_BEFORE_INSERT = 'beforeInsert';
  55. /**
  56. * @event AfterSaveEvent an event that is triggered after a record is inserted.
  57. */
  58. const EVENT_AFTER_INSERT = 'afterInsert';
  59. /**
  60. * @event ModelEvent an event that is triggered before updating a record.
  61. * You may set [[ModelEvent::isValid]] to be false to stop the update.
  62. */
  63. const EVENT_BEFORE_UPDATE = 'beforeUpdate';
  64. /**
  65. * @event AfterSaveEvent an event that is triggered after a record is updated.
  66. */
  67. const EVENT_AFTER_UPDATE = 'afterUpdate';
  68. /**
  69. * @event ModelEvent an event that is triggered before deleting a record.
  70. * You may set [[ModelEvent::isValid]] to be false to stop the deletion.
  71. */
  72. const EVENT_BEFORE_DELETE = 'beforeDelete';
  73. /**
  74. * @event Event an event that is triggered after a record is deleted.
  75. */
  76. const EVENT_AFTER_DELETE = 'afterDelete';
  77. /**
  78. * @event Event an event that is triggered after a record is refreshed.
  79. * @since 2.0.8
  80. */
  81. const EVENT_AFTER_REFRESH = 'afterRefresh';
  82. /**
  83. * @var array attribute values indexed by attribute names
  84. */
  85. private $_attributes = [];
  86. /**
  87. * @var array|null old attribute values indexed by attribute names.
  88. * This is `null` if the record [[isNewRecord|is new]].
  89. */
  90. private $_oldAttributes;
  91. /**
  92. * @var array related models indexed by the relation names
  93. */
  94. private $_related = [];
  95. /**
  96. * @inheritdoc
  97. * @return static|null ActiveRecord instance matching the condition, or `null` if nothing matches.
  98. */
  99. public static function findOne($condition)
  100. {
  101. return static::findByCondition($condition)->one();
  102. }
  103. /**
  104. * @inheritdoc
  105. * @return static[] an array of ActiveRecord instances, or an empty array if nothing matches.
  106. */
  107. public static function findAll($condition)
  108. {
  109. return static::findByCondition($condition)->all();
  110. }
  111. /**
  112. * Finds ActiveRecord instance(s) by the given condition.
  113. * This method is internally called by [[findOne()]] and [[findAll()]].
  114. * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
  115. * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
  116. * @throws InvalidConfigException if there is no primary key defined
  117. * @internal
  118. */
  119. protected static function findByCondition($condition)
  120. {
  121. $query = static::find();
  122. if (!ArrayHelper::isAssociative($condition)) {
  123. // query by primary key
  124. $primaryKey = static::primaryKey();
  125. if (isset($primaryKey[0])) {
  126. $condition = [$primaryKey[0] => $condition];
  127. } else {
  128. throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
  129. }
  130. }
  131. return $query->andWhere($condition);
  132. }
  133. /**
  134. * Updates the whole table using the provided attribute values and conditions.
  135. * For example, to change the status to be 1 for all customers whose status is 2:
  136. *
  137. * ```php
  138. * Customer::updateAll(['status' => 1], 'status = 2');
  139. * ```
  140. *
  141. * @param array $attributes attribute values (name-value pairs) to be saved into the table
  142. * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
  143. * Please refer to [[Query::where()]] on how to specify this parameter.
  144. * @return integer the number of rows updated
  145. * @throws NotSupportedException if not overridden
  146. */
  147. public static function updateAll($attributes, $condition = '')
  148. {
  149. throw new NotSupportedException(__METHOD__ . ' is not supported.');
  150. }
  151. /**
  152. * Updates the whole table using the provided counter changes and conditions.
  153. * For example, to increment all customers' age by 1,
  154. *
  155. * ```php
  156. * Customer::updateAllCounters(['age' => 1]);
  157. * ```
  158. *
  159. * @param array $counters the counters to be updated (attribute name => increment value).
  160. * Use negative values if you want to decrement the counters.
  161. * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
  162. * Please refer to [[Query::where()]] on how to specify this parameter.
  163. * @return integer the number of rows updated
  164. * @throws NotSupportedException if not overrided
  165. */
  166. public static function updateAllCounters($counters, $condition = '')
  167. {
  168. throw new NotSupportedException(__METHOD__ . ' is not supported.');
  169. }
  170. /**
  171. * Deletes rows in the table using the provided conditions.
  172. * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
  173. *
  174. * For example, to delete all customers whose status is 3:
  175. *
  176. * ```php
  177. * Customer::deleteAll('status = 3');
  178. * ```
  179. *
  180. * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
  181. * Please refer to [[Query::where()]] on how to specify this parameter.
  182. * @param array $params the parameters (name => value) to be bound to the query.
  183. * @return integer the number of rows deleted
  184. * @throws NotSupportedException if not overrided
  185. */
  186. public static function deleteAll($condition = '', $params = [])
  187. {
  188. throw new NotSupportedException(__METHOD__ . ' is not supported.');
  189. }
  190. /**
  191. * Returns the name of the column that stores the lock version for implementing optimistic locking.
  192. *
  193. * Optimistic locking allows multiple users to access the same record for edits and avoids
  194. * potential conflicts. In case when a user attempts to save the record upon some staled data
  195. * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
  196. * and the update or deletion is skipped.
  197. *
  198. * Optimistic locking is only supported by [[update()]] and [[delete()]].
  199. *
  200. * To use Optimistic locking:
  201. *
  202. * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
  203. * Override this method to return the name of this column.
  204. * 2. Add a `required` validation rule for the version column to ensure the version value is submitted.
  205. * 3. In the Web form that collects the user input, add a hidden field that stores
  206. * the lock version of the recording being updated.
  207. * 4. In the controller action that does the data updating, try to catch the [[StaleObjectException]]
  208. * and implement necessary business logic (e.g. merging the changes, prompting stated data)
  209. * to resolve the conflict.
  210. *
  211. * @return string the column name that stores the lock version of a table row.
  212. * If null is returned (default implemented), optimistic locking will not be supported.
  213. */
  214. public function optimisticLock()
  215. {
  216. return null;
  217. }
  218. /**
  219. * PHP getter magic method.
  220. * This method is overridden so that attributes and related objects can be accessed like properties.
  221. *
  222. * @param string $name property name
  223. * @throws \yii\base\InvalidParamException if relation name is wrong
  224. * @return mixed property value
  225. * @see getAttribute()
  226. */
  227. public function __get($name)
  228. {
  229. if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
  230. return $this->_attributes[$name];
  231. } elseif ($this->hasAttribute($name)) {
  232. return null;
  233. } else {
  234. if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
  235. return $this->_related[$name];
  236. }
  237. $value = parent::__get($name);
  238. if ($value instanceof ActiveQueryInterface) {
  239. return $this->_related[$name] = $value->findFor($name, $this);
  240. } else {
  241. return $value;
  242. }
  243. }
  244. }
  245. /**
  246. * PHP setter magic method.
  247. * This method is overridden so that AR attributes can be accessed like properties.
  248. * @param string $name property name
  249. * @param mixed $value property value
  250. */
  251. public function __set($name, $value)
  252. {
  253. if ($this->hasAttribute($name)) {
  254. $this->_attributes[$name] = $value;
  255. } else {
  256. parent::__set($name, $value);
  257. }
  258. }
  259. /**
  260. * Checks if a property value is null.
  261. * This method overrides the parent implementation by checking if the named attribute is null or not.
  262. * @param string $name the property name or the event name
  263. * @return boolean whether the property value is null
  264. */
  265. public function __isset($name)
  266. {
  267. try {
  268. return $this->__get($name) !== null;
  269. } catch (\Exception $e) {
  270. return false;
  271. }
  272. }
  273. /**
  274. * Sets a component property to be null.
  275. * This method overrides the parent implementation by clearing
  276. * the specified attribute value.
  277. * @param string $name the property name or the event name
  278. */
  279. public function __unset($name)
  280. {
  281. if ($this->hasAttribute($name)) {
  282. unset($this->_attributes[$name]);
  283. } elseif (array_key_exists($name, $this->_related)) {
  284. unset($this->_related[$name]);
  285. } elseif ($this->getRelation($name, false) === null) {
  286. parent::__unset($name);
  287. }
  288. }
  289. /**
  290. * Declares a `has-one` relation.
  291. * The declaration is returned in terms of a relational [[ActiveQuery]] instance
  292. * through which the related record can be queried and retrieved back.
  293. *
  294. * A `has-one` relation means that there is at most one related record matching
  295. * the criteria set by this relation, e.g., a customer has one country.
  296. *
  297. * For example, to declare the `country` relation for `Customer` class, we can write
  298. * the following code in the `Customer` class:
  299. *
  300. * ```php
  301. * public function getCountry()
  302. * {
  303. * return $this->hasOne(Country::className(), ['id' => 'country_id']);
  304. * }
  305. * ```
  306. *
  307. * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name
  308. * in the related class `Country`, while the 'country_id' value refers to an attribute name
  309. * in the current AR class.
  310. *
  311. * Call methods declared in [[ActiveQuery]] to further customize the relation.
  312. *
  313. * @param string $class the class name of the related record
  314. * @param array $link the primary-foreign key constraint. The keys of the array refer to
  315. * the attributes of the record associated with the `$class` model, while the values of the
  316. * array refer to the corresponding attributes in **this** AR class.
  317. * @return ActiveQueryInterface the relational query object.
  318. */
  319. public function hasOne($class, $link)
  320. {
  321. /* @var $class ActiveRecordInterface */
  322. /* @var $query ActiveQuery */
  323. $query = $class::find();
  324. $query->primaryModel = $this;
  325. $query->link = $link;
  326. $query->multiple = false;
  327. return $query;
  328. }
  329. /**
  330. * Declares a `has-many` relation.
  331. * The declaration is returned in terms of a relational [[ActiveQuery]] instance
  332. * through which the related record can be queried and retrieved back.
  333. *
  334. * A `has-many` relation means that there are multiple related records matching
  335. * the criteria set by this relation, e.g., a customer has many orders.
  336. *
  337. * For example, to declare the `orders` relation for `Customer` class, we can write
  338. * the following code in the `Customer` class:
  339. *
  340. * ```php
  341. * public function getOrders()
  342. * {
  343. * return $this->hasMany(Order::className(), ['customer_id' => 'id']);
  344. * }
  345. * ```
  346. *
  347. * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
  348. * an attribute name in the related class `Order`, while the 'id' value refers to
  349. * an attribute name in the current AR class.
  350. *
  351. * Call methods declared in [[ActiveQuery]] to further customize the relation.
  352. *
  353. * @param string $class the class name of the related record
  354. * @param array $link the primary-foreign key constraint. The keys of the array refer to
  355. * the attributes of the record associated with the `$class` model, while the values of the
  356. * array refer to the corresponding attributes in **this** AR class.
  357. * @return ActiveQueryInterface the relational query object.
  358. */
  359. public function hasMany($class, $link)
  360. {
  361. /* @var $class ActiveRecordInterface */
  362. /* @var $query ActiveQuery */
  363. $query = $class::find();
  364. $query->primaryModel = $this;
  365. $query->link = $link;
  366. $query->multiple = true;
  367. return $query;
  368. }
  369. /**
  370. * Populates the named relation with the related records.
  371. * Note that this method does not check if the relation exists or not.
  372. * @param string $name the relation name (case-sensitive)
  373. * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
  374. */
  375. public function populateRelation($name, $records)
  376. {
  377. $this->_related[$name] = $records;
  378. }
  379. /**
  380. * Check whether the named relation has been populated with records.
  381. * @param string $name the relation name (case-sensitive)
  382. * @return boolean whether relation has been populated with records.
  383. */
  384. public function isRelationPopulated($name)
  385. {
  386. return array_key_exists($name, $this->_related);
  387. }
  388. /**
  389. * Returns all populated related records.
  390. * @return array an array of related records indexed by relation names.
  391. */
  392. public function getRelatedRecords()
  393. {
  394. return $this->_related;
  395. }
  396. /**
  397. * Returns a value indicating whether the model has an attribute with the specified name.
  398. * @param string $name the name of the attribute
  399. * @return boolean whether the model has an attribute with the specified name.
  400. */
  401. public function hasAttribute($name)
  402. {
  403. return isset($this->_attributes[$name]) || in_array($name, $this->attributes(), true);
  404. }
  405. /**
  406. * Returns the named attribute value.
  407. * If this record is the result of a query and the attribute is not loaded,
  408. * null will be returned.
  409. * @param string $name the attribute name
  410. * @return mixed the attribute value. Null if the attribute is not set or does not exist.
  411. * @see hasAttribute()
  412. */
  413. public function getAttribute($name)
  414. {
  415. return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
  416. }
  417. /**
  418. * Sets the named attribute value.
  419. * @param string $name the attribute name
  420. * @param mixed $value the attribute value.
  421. * @throws InvalidParamException if the named attribute does not exist.
  422. * @see hasAttribute()
  423. */
  424. public function setAttribute($name, $value)
  425. {
  426. if ($this->hasAttribute($name)) {
  427. $this->_attributes[$name] = $value;
  428. } else {
  429. throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
  430. }
  431. }
  432. /**
  433. * Returns the old attribute values.
  434. * @return array the old attribute values (name-value pairs)
  435. */
  436. public function getOldAttributes()
  437. {
  438. return $this->_oldAttributes === null ? [] : $this->_oldAttributes;
  439. }
  440. /**
  441. * Sets the old attribute values.
  442. * All existing old attribute values will be discarded.
  443. * @param array|null $values old attribute values to be set.
  444. * If set to `null` this record is considered to be [[isNewRecord|new]].
  445. */
  446. public function setOldAttributes($values)
  447. {
  448. $this->_oldAttributes = $values;
  449. }
  450. /**
  451. * Returns the old value of the named attribute.
  452. * If this record is the result of a query and the attribute is not loaded,
  453. * null will be returned.
  454. * @param string $name the attribute name
  455. * @return mixed the old attribute value. Null if the attribute is not loaded before
  456. * or does not exist.
  457. * @see hasAttribute()
  458. */
  459. public function getOldAttribute($name)
  460. {
  461. return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
  462. }
  463. /**
  464. * Sets the old value of the named attribute.
  465. * @param string $name the attribute name
  466. * @param mixed $value the old attribute value.
  467. * @throws InvalidParamException if the named attribute does not exist.
  468. * @see hasAttribute()
  469. */
  470. public function setOldAttribute($name, $value)
  471. {
  472. if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) {
  473. $this->_oldAttributes[$name] = $value;
  474. } else {
  475. throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".');
  476. }
  477. }
  478. /**
  479. * Marks an attribute dirty.
  480. * This method may be called to force updating a record when calling [[update()]],
  481. * even if there is no change being made to the record.
  482. * @param string $name the attribute name
  483. */
  484. public function markAttributeDirty($name)
  485. {
  486. unset($this->_oldAttributes[$name]);
  487. }
  488. /**
  489. * Returns a value indicating whether the named attribute has been changed.
  490. * @param string $name the name of the attribute.
  491. * @param boolean $identical whether the comparison of new and old value is made for
  492. * identical values using `===`, defaults to `true`. Otherwise `==` is used for comparison.
  493. * This parameter is available since version 2.0.4.
  494. * @return boolean whether the attribute has been changed
  495. */
  496. public function isAttributeChanged($name, $identical = true)
  497. {
  498. if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
  499. if ($identical) {
  500. return $this->_attributes[$name] !== $this->_oldAttributes[$name];
  501. } else {
  502. return $this->_attributes[$name] != $this->_oldAttributes[$name];
  503. }
  504. } else {
  505. return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
  506. }
  507. }
  508. /**
  509. * Returns the attribute values that have been modified since they are loaded or saved most recently.
  510. *
  511. * The comparison of new and old values is made for identical values using `===`.
  512. *
  513. * @param string[]|null $names the names of the attributes whose values may be returned if they are
  514. * changed recently. If null, [[attributes()]] will be used.
  515. * @return array the changed attribute values (name-value pairs)
  516. */
  517. public function getDirtyAttributes($names = null)
  518. {
  519. if ($names === null) {
  520. $names = $this->attributes();
  521. }
  522. $names = array_flip($names);
  523. $attributes = [];
  524. if ($this->_oldAttributes === null) {
  525. foreach ($this->_attributes as $name => $value) {
  526. if (isset($names[$name])) {
  527. $attributes[$name] = $value;
  528. }
  529. }
  530. } else {
  531. foreach ($this->_attributes as $name => $value) {
  532. if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) {
  533. $attributes[$name] = $value;
  534. }
  535. }
  536. }
  537. return $attributes;
  538. }
  539. /**
  540. * Saves the current record.
  541. *
  542. * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]]
  543. * when [[isNewRecord]] is false.
  544. *
  545. * For example, to save a customer record:
  546. *
  547. * ```php
  548. * $customer = new Customer; // or $customer = Customer::findOne($id);
  549. * $customer->name = $name;
  550. * $customer->email = $email;
  551. * $customer->save();
  552. * ```
  553. *
  554. * @param boolean $runValidation whether to perform validation (calling [[validate()]])
  555. * before saving the record. Defaults to `true`. If the validation fails, the record
  556. * will not be saved to the database and this method will return `false`.
  557. * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
  558. * meaning all attributes that are loaded from DB will be saved.
  559. * @return boolean whether the saving succeeded (i.e. no validation errors occurred).
  560. */
  561. public function save($runValidation = true, $attributeNames = null)
  562. {
  563. if ($this->getIsNewRecord()) {
  564. return $this->insert($runValidation, $attributeNames);
  565. } else {
  566. return $this->update($runValidation, $attributeNames) !== false;
  567. }
  568. }
  569. /**
  570. * Saves the changes to this active record into the associated database table.
  571. *
  572. * This method performs the following steps in order:
  573. *
  574. * 1. call [[beforeValidate()]] when `$runValidation` is true. If [[beforeValidate()]]
  575. * returns `false`, the rest of the steps will be skipped;
  576. * 2. call [[afterValidate()]] when `$runValidation` is true. If validation
  577. * failed, the rest of the steps will be skipped;
  578. * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
  579. * the rest of the steps will be skipped;
  580. * 4. save the record into database. If this fails, it will skip the rest of the steps;
  581. * 5. call [[afterSave()]];
  582. *
  583. * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
  584. * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
  585. * will be raised by the corresponding methods.
  586. *
  587. * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
  588. *
  589. * For example, to update a customer record:
  590. *
  591. * ```php
  592. * $customer = Customer::findOne($id);
  593. * $customer->name = $name;
  594. * $customer->email = $email;
  595. * $customer->update();
  596. * ```
  597. *
  598. * Note that it is possible the update does not affect any row in the table.
  599. * In this case, this method will return 0. For this reason, you should use the following
  600. * code to check if update() is successful or not:
  601. *
  602. * ```php
  603. * if ($customer->update() !== false) {
  604. * // update successful
  605. * } else {
  606. * // update failed
  607. * }
  608. * ```
  609. *
  610. * @param boolean $runValidation whether to perform validation (calling [[validate()]])
  611. * before saving the record. Defaults to `true`. If the validation fails, the record
  612. * will not be saved to the database and this method will return `false`.
  613. * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
  614. * meaning all attributes that are loaded from DB will be saved.
  615. * @return integer|boolean the number of rows affected, or false if validation fails
  616. * or [[beforeSave()]] stops the updating process.
  617. * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
  618. * being updated is outdated.
  619. * @throws Exception in case update failed.
  620. */
  621. public function update($runValidation = true, $attributeNames = null)
  622. {
  623. if ($runValidation && !$this->validate($attributeNames)) {
  624. return false;
  625. }
  626. return $this->updateInternal($attributeNames);
  627. }
  628. /**
  629. * Updates the specified attributes.
  630. *
  631. * This method is a shortcut to [[update()]] when data validation is not needed
  632. * and only a small set attributes need to be updated.
  633. *
  634. * You may specify the attributes to be updated as name list or name-value pairs.
  635. * If the latter, the corresponding attribute values will be modified accordingly.
  636. * The method will then save the specified attributes into database.
  637. *
  638. * Note that this method will **not** perform data validation and will **not** trigger events.
  639. *
  640. * @param array $attributes the attributes (names or name-value pairs) to be updated
  641. * @return integer the number of rows affected.
  642. */
  643. public function updateAttributes($attributes)
  644. {
  645. $attrs = [];
  646. foreach ($attributes as $name => $value) {
  647. if (is_int($name)) {
  648. $attrs[] = $value;
  649. } else {
  650. $this->$name = $value;
  651. $attrs[] = $name;
  652. }
  653. }
  654. $values = $this->getDirtyAttributes($attrs);
  655. if (empty($values)) {
  656. return 0;
  657. }
  658. $rows = static::updateAll($values, $this->getOldPrimaryKey(true));
  659. foreach ($values as $name => $value) {
  660. $this->_oldAttributes[$name] = $this->_attributes[$name];
  661. }
  662. return $rows;
  663. }
  664. /**
  665. * @see update()
  666. * @param array $attributes attributes to update
  667. * @return integer number of rows updated
  668. * @throws StaleObjectException
  669. */
  670. protected function updateInternal($attributes = null)
  671. {
  672. if (!$this->beforeSave(false)) {
  673. return false;
  674. }
  675. $values = $this->getDirtyAttributes($attributes);
  676. if (empty($values)) {
  677. $this->afterSave(false, $values);
  678. return 0;
  679. }
  680. $condition = $this->getOldPrimaryKey(true);
  681. $lock = $this->optimisticLock();
  682. if ($lock !== null) {
  683. $values[$lock] = $this->$lock + 1;
  684. $condition[$lock] = $this->$lock;
  685. }
  686. // We do not check the return value of updateAll() because it's possible
  687. // that the UPDATE statement doesn't change anything and thus returns 0.
  688. $rows = static::updateAll($values, $condition);
  689. if ($lock !== null && !$rows) {
  690. throw new StaleObjectException('The object being updated is outdated.');
  691. }
  692. if (isset($values[$lock])) {
  693. $this->$lock = $values[$lock];
  694. }
  695. $changedAttributes = [];
  696. foreach ($values as $name => $value) {
  697. $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
  698. $this->_oldAttributes[$name] = $value;
  699. }
  700. $this->afterSave(false, $changedAttributes);
  701. return $rows;
  702. }
  703. /**
  704. * Updates one or several counter columns for the current AR object.
  705. * Note that this method differs from [[updateAllCounters()]] in that it only
  706. * saves counters for the current AR object.
  707. *
  708. * An example usage is as follows:
  709. *
  710. * ```php
  711. * $post = Post::findOne($id);
  712. * $post->updateCounters(['view_count' => 1]);
  713. * ```
  714. *
  715. * @param array $counters the counters to be updated (attribute name => increment value)
  716. * Use negative values if you want to decrement the counters.
  717. * @return boolean whether the saving is successful
  718. * @see updateAllCounters()
  719. */
  720. public function updateCounters($counters)
  721. {
  722. if (static::updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
  723. foreach ($counters as $name => $value) {
  724. if (!isset($this->_attributes[$name])) {
  725. $this->_attributes[$name] = $value;
  726. } else {
  727. $this->_attributes[$name] += $value;
  728. }
  729. $this->_oldAttributes[$name] = $this->_attributes[$name];
  730. }
  731. return true;
  732. } else {
  733. return false;
  734. }
  735. }
  736. /**
  737. * Deletes the table row corresponding to this active record.
  738. *
  739. * This method performs the following steps in order:
  740. *
  741. * 1. call [[beforeDelete()]]. If the method returns false, it will skip the
  742. * rest of the steps;
  743. * 2. delete the record from the database;
  744. * 3. call [[afterDelete()]].
  745. *
  746. * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
  747. * will be raised by the corresponding methods.
  748. *
  749. * @return integer|false the number of rows deleted, or false if the deletion is unsuccessful for some reason.
  750. * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
  751. * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
  752. * being deleted is outdated.
  753. * @throws Exception in case delete failed.
  754. */
  755. public function delete()
  756. {
  757. $result = false;
  758. if ($this->beforeDelete()) {
  759. // we do not check the return value of deleteAll() because it's possible
  760. // the record is already deleted in the database and thus the method will return 0
  761. $condition = $this->getOldPrimaryKey(true);
  762. $lock = $this->optimisticLock();
  763. if ($lock !== null) {
  764. $condition[$lock] = $this->$lock;
  765. }
  766. $result = static::deleteAll($condition);
  767. if ($lock !== null && !$result) {
  768. throw new StaleObjectException('The object being deleted is outdated.');
  769. }
  770. $this->_oldAttributes = null;
  771. $this->afterDelete();
  772. }
  773. return $result;
  774. }
  775. /**
  776. * Returns a value indicating whether the current record is new.
  777. * @return boolean whether the record is new and should be inserted when calling [[save()]].
  778. */
  779. public function getIsNewRecord()
  780. {
  781. return $this->_oldAttributes === null;
  782. }
  783. /**
  784. * Sets the value indicating whether the record is new.
  785. * @param boolean $value whether the record is new and should be inserted when calling [[save()]].
  786. * @see getIsNewRecord()
  787. */
  788. public function setIsNewRecord($value)
  789. {
  790. $this->_oldAttributes = $value ? null : $this->_attributes;
  791. }
  792. /**
  793. * Initializes the object.
  794. * This method is called at the end of the constructor.
  795. * The default implementation will trigger an [[EVENT_INIT]] event.
  796. * If you override this method, make sure you call the parent implementation at the end
  797. * to ensure triggering of the event.
  798. */
  799. public function init()
  800. {
  801. parent::init();
  802. $this->trigger(self::EVENT_INIT);
  803. }
  804. /**
  805. * This method is called when the AR object is created and populated with the query result.
  806. * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
  807. * When overriding this method, make sure you call the parent implementation to ensure the
  808. * event is triggered.
  809. */
  810. public function afterFind()
  811. {
  812. $this->trigger(self::EVENT_AFTER_FIND);
  813. }
  814. /**
  815. * This method is called at the beginning of inserting or updating a record.
  816. * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true,
  817. * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false.
  818. * When overriding this method, make sure you call the parent implementation like the following:
  819. *
  820. * ```php
  821. * public function beforeSave($insert)
  822. * {
  823. * if (parent::beforeSave($insert)) {
  824. * // ...custom code here...
  825. * return true;
  826. * } else {
  827. * return false;
  828. * }
  829. * }
  830. * ```
  831. *
  832. * @param boolean $insert whether this method called while inserting a record.
  833. * If false, it means the method is called while updating a record.
  834. * @return boolean whether the insertion or updating should continue.
  835. * If false, the insertion or updating will be cancelled.
  836. */
  837. public function beforeSave($insert)
  838. {
  839. $event = new ModelEvent;
  840. $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
  841. return $event->isValid;
  842. }
  843. /**
  844. * This method is called at the end of inserting or updating a record.
  845. * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true,
  846. * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. The event class used is [[AfterSaveEvent]].
  847. * When overriding this method, make sure you call the parent implementation so that
  848. * the event is triggered.
  849. * @param boolean $insert whether this method called while inserting a record.
  850. * If false, it means the method is called while updating a record.
  851. * @param array $changedAttributes The old values of attributes that had changed and were saved.
  852. * You can use this parameter to take action based on the changes made for example send an email
  853. * when the password had changed or implement audit trail that tracks all the changes.
  854. * `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
  855. * already the new, updated values.
  856. */
  857. public function afterSave($insert, $changedAttributes)
  858. {
  859. $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([
  860. 'changedAttributes' => $changedAttributes,
  861. ]));
  862. }
  863. /**
  864. * This method is invoked before deleting a record.
  865. * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
  866. * When overriding this method, make sure you call the parent implementation like the following:
  867. *
  868. * ```php
  869. * public function beforeDelete()
  870. * {
  871. * if (parent::beforeDelete()) {
  872. * // ...custom code here...
  873. * return true;
  874. * } else {
  875. * return false;
  876. * }
  877. * }
  878. * ```
  879. *
  880. * @return boolean whether the record should be deleted. Defaults to true.
  881. */
  882. public function beforeDelete()
  883. {
  884. $event = new ModelEvent;
  885. $this->trigger(self::EVENT_BEFORE_DELETE, $event);
  886. return $event->isValid;
  887. }
  888. /**
  889. * This method is invoked after deleting a record.
  890. * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
  891. * You may override this method to do postprocessing after the record is deleted.
  892. * Make sure you call the parent implementation so that the event is raised properly.
  893. */
  894. public function afterDelete()
  895. {
  896. $this->trigger(self::EVENT_AFTER_DELETE);
  897. }
  898. /**
  899. * Repopulates this active record with the latest data.
  900. *
  901. * If the refresh is successful, an [[EVENT_AFTER_REFRESH]] event will be triggered.
  902. * This event is available since version 2.0.8.
  903. *
  904. * @return boolean whether the row still exists in the database. If true, the latest data
  905. * will be populated to this active record. Otherwise, this record will remain unchanged.
  906. */
  907. public function refresh()
  908. {
  909. /* @var $record BaseActiveRecord */
  910. $record = static::findOne($this->getPrimaryKey(true));
  911. if ($record === null) {
  912. return false;
  913. }
  914. foreach ($this->attributes() as $name) {
  915. $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
  916. }
  917. $this->_oldAttributes = $this->_attributes;
  918. $this->_related = [];
  919. $this->afterRefresh();
  920. return true;
  921. }
  922. /**
  923. * This method is called when the AR object is refreshed.
  924. * The default implementation will trigger an [[EVENT_AFTER_REFRESH]] event.
  925. * When overriding this method, make sure you call the parent implementation to ensure the
  926. * event is triggered.
  927. * @since 2.0.8
  928. */
  929. public function afterRefresh()
  930. {
  931. $this->trigger(self::EVENT_AFTER_REFRESH);
  932. }
  933. /**
  934. * Returns a value indicating whether the given active record is the same as the current one.
  935. * The comparison is made by comparing the table names and the primary key values of the two active records.
  936. * If one of the records [[isNewRecord|is new]] they are also considered not equal.
  937. * @param ActiveRecordInterface $record record to compare to
  938. * @return boolean whether the two active records refer to the same row in the same database table.
  939. */
  940. public function equals($record)
  941. {
  942. if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
  943. return false;
  944. }
  945. return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
  946. }
  947. /**
  948. * Returns the primary key value(s).
  949. * @param boolean $asArray whether to return the primary key value as an array. If true,
  950. * the return value will be an array with column names as keys and column values as values.
  951. * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
  952. * @property mixed The primary key value. An array (column name => column value) is returned if
  953. * the primary key is composite. A string is returned otherwise (null will be returned if
  954. * the key value is null).
  955. * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
  956. * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
  957. * the key value is null).
  958. */
  959. public function getPrimaryKey($asArray = false)
  960. {
  961. $keys = $this->primaryKey();
  962. if (!$asArray && count($keys) === 1) {
  963. return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
  964. } else {
  965. $values = [];
  966. foreach ($keys as $name) {
  967. $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
  968. }
  969. return $values;
  970. }
  971. }
  972. /**
  973. * Returns the old primary key value(s).
  974. * This refers to the primary key value that is populated into the record
  975. * after executing a find method (e.g. find(), findOne()).
  976. * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
  977. * @param boolean $asArray whether to return the primary key value as an array. If true,
  978. * the return value will be an array with column name as key and column value as value.
  979. * If this is false (default), a scalar value will be returned for non-composite primary key.
  980. * @property mixed The old primary key value. An array (column name => column value) is
  981. * returned if the primary key is composite. A string is returned otherwise (null will be
  982. * returned if the key value is null).
  983. * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
  984. * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
  985. * the key value is null).
  986. * @throws Exception if the AR model does not have a primary key
  987. */
  988. public function getOldPrimaryKey($asArray = false)
  989. {
  990. $keys = $this->primaryKey();
  991. if (empty($keys)) {
  992. throw new Exception(get_class($this) . ' does not have a primary key. You should either define a primary key for the corresponding table or override the primaryKey() method.');
  993. }
  994. if (!$asArray && count($keys) === 1) {
  995. return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
  996. } else {
  997. $values = [];
  998. foreach ($keys as $name) {
  999. $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
  1000. }
  1001. return $values;
  1002. }
  1003. }
  1004. /**
  1005. * Populates an active record object using a row of data from the database/storage.
  1006. *
  1007. * This is an internal method meant to be called to create active record objects after
  1008. * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
  1009. * the query results into active records.
  1010. *
  1011. * When calling this method manually you should call [[afterFind()]] on the created
  1012. * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
  1013. *
  1014. * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
  1015. * created by [[instantiate()]] beforehand.
  1016. * @param array $row attribute values (name => value)
  1017. */
  1018. public static function populateRecord($record, $row)
  1019. {
  1020. $columns = array_flip($record->attributes());
  1021. foreach ($row as $name => $value) {
  1022. if (isset($columns[$name])) {
  1023. $record->_attributes[$name] = $value;
  1024. } elseif ($record->canSetProperty($name)) {
  1025. $record->$name = $value;
  1026. }
  1027. }
  1028. $record->_oldAttributes = $record->_attributes;
  1029. }
  1030. /**
  1031. * Creates an active record instance.
  1032. *
  1033. * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
  1034. * It is not meant to be used for creating new records directly.
  1035. *
  1036. * You may override this method if the instance being created
  1037. * depends on the row data to be populated into the record.
  1038. * For example, by creating a record based on the value of a column,
  1039. * you may implement the so-called single-table inheritance mapping.
  1040. * @param array $row row data to be populated into the record.
  1041. * @return static the newly created active record
  1042. */
  1043. public static function instantiate($row)
  1044. {
  1045. return new static;
  1046. }
  1047. /**
  1048. * Returns whether there is an element at the specified offset.
  1049. * This method is required by the interface [[\ArrayAccess]].
  1050. * @param mixed $offset the offset to check on
  1051. * @return boolean whether there is an element at the specified offset.
  1052. */
  1053. public function offsetExists($offset)
  1054. {
  1055. return $this->__isset($offset);
  1056. }
  1057. /**
  1058. * Returns the relation object with the specified name.
  1059. * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object.
  1060. * It can be declared in either the Active Record class itself or one of its behaviors.
  1061. * @param string $name the relation name
  1062. * @param boolean $throwException whether to throw exception if the relation does not exist.
  1063. * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
  1064. * and `$throwException` is false, null will be returned.
  1065. * @throws InvalidParamException if the named relation does not exist.
  1066. */
  1067. public function getRelation($name, $throwException = true)
  1068. {
  1069. $getter = 'get' . $name;
  1070. try {
  1071. // the relation could be defined in a behavior
  1072. $relation = $this->$getter();
  1073. } catch (UnknownMethodException $e) {
  1074. if ($throwException) {
  1075. throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
  1076. } else {
  1077. return null;
  1078. }
  1079. }
  1080. if (!$relation instanceof ActiveQueryInterface) {
  1081. if ($throwException) {
  1082. throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
  1083. } else {
  1084. return null;
  1085. }
  1086. }
  1087. if (method_exists($this, $getter)) {
  1088. // relation name is case sensitive, trying to validate it when the relation is defined within this class
  1089. $method = new \ReflectionMethod($this, $getter);
  1090. $realName = lcfirst(substr($method->getName(), 3));
  1091. if ($realName !== $name) {
  1092. if ($throwException) {
  1093. throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
  1094. } else {
  1095. return null;
  1096. }
  1097. }
  1098. }
  1099. return $relation;
  1100. }
  1101. /**
  1102. * Establishes the relationship between two models.
  1103. *
  1104. * The relationship is established by setting the foreign key value(s) in one model
  1105. * to be the corresponding primary key value(s) in the other model.
  1106. * The model with the foreign key will be saved into database without performing validation.
  1107. *
  1108. * If the relationship involves a junction table, a new row will be inserted into the
  1109. * junction table which contains the primary key values from both models.
  1110. *
  1111. * Note that this method requires that the primary key value is not null.
  1112. *
  1113. * @param string $name the case sensitive name of the relationship.
  1114. * @param ActiveRecordInterface $model the model to be linked with the current one.
  1115. * @param array $extraColumns additional column values to be saved into the junction table.
  1116. * This parameter is only meaningful for a relationship involving a junction table
  1117. * (i.e., a relation set with [[ActiveRelationTrait::via()]] or [[ActiveQuery::viaTable()]].)
  1118. * @throws InvalidCallException if the method is unable to link two models.
  1119. */
  1120. public function link($name, $model, $extraColumns = [])
  1121. {
  1122. $relation = $this->getRelation($name);
  1123. if ($relation->via !== null) {
  1124. if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
  1125. throw new InvalidCallException('Unable to link models: the models being linked cannot be newly created.');
  1126. }
  1127. if (is_array($relation->via)) {
  1128. /* @var $viaRelation ActiveQuery */
  1129. list($viaName, $viaRelation) = $relation->via;
  1130. $viaClass = $viaRelation->modelClass;
  1131. // unset $viaName so that it can be reloaded to reflect the change
  1132. unset($this->_related[$viaName]);
  1133. } else {
  1134. $viaRelation = $relation->via;
  1135. $viaTable = reset($relation->via->from);
  1136. }
  1137. $columns = [];
  1138. foreach ($viaRelation->link as $a => $b) {
  1139. $columns[$a] = $this->$b;
  1140. }
  1141. foreach ($relation->link as $a => $b) {
  1142. $columns[$b] = $model->$a;
  1143. }
  1144. foreach ($extraColumns as $k => $v) {
  1145. $columns[$k] = $v;
  1146. }
  1147. if (is_array($relation->via)) {
  1148. /* @var $viaClass ActiveRecordInterface */
  1149. /* @var $record ActiveRecordInterface */
  1150. $record = new $viaClass();
  1151. foreach ($columns as $column => $value) {
  1152. $record->$column = $value;
  1153. }
  1154. $record->insert(false);
  1155. } else {
  1156. /* @var $viaTable string */
  1157. static::getDb()->createCommand()
  1158. ->insert($viaTable, $columns)->execute();
  1159. }
  1160. } else {
  1161. $p1 = $model->isPrimaryKey(array_keys($relation->link));
  1162. $p2 = static::isPrimaryKey(array_values($relation->link));
  1163. if ($p1 && $p2) {
  1164. if ($this->getIsNewRecord() && $model->getIsNewRecord()) {
  1165. throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
  1166. } elseif ($this->getIsNewRecord()) {
  1167. $this->bindModels(array_flip($relation->link), $this, $model);
  1168. } else {
  1169. $this->bindModels($relation->link, $model, $this);
  1170. }
  1171. } elseif ($p1) {
  1172. $this->bindModels(array_flip($relation->link), $this, $model);
  1173. } elseif ($p2) {
  1174. $this->bindModels($relation->link, $model, $this);
  1175. } else {
  1176. throw new InvalidCallException('Unable to link models: the link defining the relation does not involve any primary key.');
  1177. }
  1178. }
  1179. // update lazily loaded related objects
  1180. if (!$relation->multiple) {
  1181. $this->_related[$name] = $model;
  1182. } elseif (isset($this->_related[$name])) {
  1183. if ($relation->indexBy !== null) {
  1184. if ($relation->indexBy instanceof \Closure) {
  1185. $index = call_user_func($relation->indexBy, $model);
  1186. } else {
  1187. $index = $model->{$relation->indexBy};
  1188. }
  1189. $this->_related[$name][$index] = $model;
  1190. } else {
  1191. $this->_related[$name][] = $model;
  1192. }
  1193. }
  1194. }
  1195. /**
  1196. * Destroys the relationship between two models.
  1197. *
  1198. * The model with the foreign key of the relationship will be deleted if `$delete` is true.
  1199. * Otherwise, the foreign key will be set null and the model will be saved without validation.
  1200. *
  1201. * @param string $name the case sensitive name of the relationship.
  1202. * @param ActiveRecordInterface $model the model to be unlinked from the current one.
  1203. * You have to make sure that the model is really related with the current model as this method
  1204. * does not check this.
  1205. * @param boolean $delete whether to delete the model that contains the foreign key.
  1206. * If false, the model's foreign key will be set null and saved.
  1207. * If true, the model containing the foreign key will be deleted.
  1208. * @throws InvalidCallException if the models cannot be unlinked
  1209. */
  1210. public function unlink($name, $model, $delete = false)
  1211. {
  1212. $relation = $this->getRelation($name);
  1213. if ($relation->via !== null) {
  1214. if (is_array($relation->via)) {
  1215. /* @var $viaRelation ActiveQuery */
  1216. list($viaName, $viaRelation) = $relation->via;
  1217. $viaClass = $viaRelation->modelClass;
  1218. unset($this->_related[$viaName]);
  1219. } else {
  1220. $viaRelation = $relation->via;
  1221. $viaTable = reset($relation->via->from);
  1222. }
  1223. $columns = [];
  1224. foreach ($viaRelation->link as $a => $b) {
  1225. $columns[$a] = $this->$b;
  1226. }
  1227. foreach ($relation->link as $a => $b) {
  1228. $columns[$b] = $model->$a;
  1229. }
  1230. $nulls = [];
  1231. foreach (array_keys($columns) as $a) {
  1232. $nulls[$a] = null;
  1233. }
  1234. if (is_array($relation->via)) {
  1235. /* @var $viaClass ActiveRecordInterface */
  1236. if ($delete) {
  1237. $viaClass::deleteAll($columns);
  1238. } else {
  1239. $viaClass::updateAll($nulls, $columns);
  1240. }
  1241. } else {
  1242. /* @var $viaTable string */
  1243. /* @var $command Command */
  1244. $command = static::getDb()->createCommand();
  1245. if ($delete) {
  1246. $command->delete($viaTable, $columns)->execute();
  1247. } else {
  1248. $command->update($viaTable, $nulls, $columns)->execute();
  1249. }
  1250. }
  1251. } else {
  1252. $p1 = $model->isPrimaryKey(array_keys($relation->link));
  1253. $p2 = static::isPrimaryKey(array_values($relation->link));
  1254. if ($p2) {
  1255. if ($delete) {
  1256. $model->delete();
  1257. } else {
  1258. foreach ($relation->link as $a => $b) {
  1259. $model->$a = null;
  1260. }
  1261. $model->save(false);
  1262. }
  1263. } elseif ($p1) {
  1264. foreach ($relation->link as $a => $b) {
  1265. if (is_array($this->$b)) { // relation via array valued attribute
  1266. if (($key = array_search($model->$a, $this->$b, false)) !== false) {
  1267. $values = $this->$b;
  1268. unset($values[$key]);
  1269. $this->$b = array_values($values);
  1270. }
  1271. } else {
  1272. $this->$b = null;
  1273. }
  1274. }
  1275. $delete ? $this->delete() : $this->save(false);
  1276. } else {
  1277. throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
  1278. }
  1279. }
  1280. if (!$relation->multiple) {
  1281. unset($this->_related[$name]);
  1282. } elseif (isset($this->_related[$name])) {
  1283. /* @var $b ActiveRecordInterface */
  1284. foreach ($this->_related[$name] as $a => $b) {
  1285. if ($model->getPrimaryKey() === $b->getPrimaryKey()) {
  1286. unset($this->_related[$name][$a]);
  1287. }
  1288. }
  1289. }
  1290. }
  1291. /**
  1292. * Destroys the relationship in current model.
  1293. *
  1294. * The model with the foreign key of the relationship will be deleted if `$delete` is true.
  1295. * Otherwise, the foreign key will be set null and the model will be saved without validation.
  1296. *
  1297. * Note that to destroy the relationship without removing records make sure your keys can be set to null
  1298. *
  1299. * @param string $name the case sensitive name of the relationship.
  1300. * @param boolean $delete whether to delete the model that contains the foreign key.
  1301. */
  1302. public function unlinkAll($name, $delete = false)
  1303. {
  1304. $relation = $this->getRelation($name);
  1305. if ($relation->via !== null) {
  1306. if (is_array($relation->via)) {
  1307. /* @var $viaRelation ActiveQuery */
  1308. list($viaName, $viaRelation) = $relation->via;
  1309. $viaClass = $viaRelation->modelClass;
  1310. unset($this->_related[$viaName]);
  1311. } else {
  1312. $viaRelation = $relation->via;
  1313. $viaTable = reset($relation->via->from);
  1314. }
  1315. $condition = [];
  1316. $nulls = [];
  1317. foreach ($viaRelation->link as $a => $b) {
  1318. $nulls[$a] = null;
  1319. $condition[$a] = $this->$b;
  1320. }
  1321. if (!empty($viaRelation->where)) {
  1322. $condition = ['and', $condition, $viaRelation->where];
  1323. }
  1324. if (is_array($relation->via)) {
  1325. /* @var $viaClass ActiveRecordInterface */
  1326. if ($delete) {
  1327. $viaClass::deleteAll($condition);
  1328. } else {
  1329. $viaClass::updateAll($nulls, $condition);
  1330. }
  1331. } else {
  1332. /* @var $viaTable string */
  1333. /* @var $command Command */
  1334. $command = static::getDb()->createCommand();
  1335. if ($delete) {
  1336. $command->delete($viaTable, $condition)->execute();
  1337. } else {
  1338. $command->update($viaTable, $nulls, $condition)->execute();
  1339. }
  1340. }
  1341. } else {
  1342. /* @var $relatedModel ActiveRecordInterface */
  1343. $relatedModel = $relation->modelClass;
  1344. if (!$delete && count($relation->link) === 1 && is_array($this->{$b = reset($relation->link)})) {
  1345. // relation via array valued attribute
  1346. $this->$b = [];
  1347. $this->save(false);
  1348. } else {
  1349. $nulls = [];
  1350. $condition = [];
  1351. foreach ($relation->link as $a => $b) {
  1352. $nulls[$a] = null;
  1353. $condition[$a] = $this->$b;
  1354. }
  1355. if (!empty($relation->where)) {
  1356. $condition = ['and', $condition, $relation->where];
  1357. }
  1358. if ($delete) {
  1359. $relatedModel::deleteAll($condition);
  1360. } else {
  1361. $relatedModel::updateAll($nulls, $condition);
  1362. }
  1363. }
  1364. }
  1365. unset($this->_related[$name]);
  1366. }
  1367. /**
  1368. * @param array $link
  1369. * @param ActiveRecordInterface $foreignModel
  1370. * @param ActiveRecordInterface $primaryModel
  1371. * @throws InvalidCallException
  1372. */
  1373. private function bindModels($link, $foreignModel, $primaryModel)
  1374. {
  1375. foreach ($link as $fk => $pk) {
  1376. $value = $primaryModel->$pk;
  1377. if ($value === null) {
  1378. throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
  1379. }
  1380. if (is_array($foreignModel->$fk)) { // relation via array valued attribute
  1381. $foreignModel->$fk = array_merge($foreignModel->$fk, [$value]);
  1382. } else {
  1383. $foreignModel->$fk = $value;
  1384. }
  1385. }
  1386. $foreignModel->save(false);
  1387. }
  1388. /**
  1389. * Returns a value indicating whether the given set of attributes represents the primary key for this model
  1390. * @param array $keys the set of attributes to check
  1391. * @return boolean whether the given set of attributes represents the primary key for this model
  1392. */
  1393. public static function isPrimaryKey($keys)
  1394. {
  1395. $pks = static::primaryKey();
  1396. if (count($keys) === count($pks)) {
  1397. return count(array_intersect($keys, $pks)) === count($pks);
  1398. } else {
  1399. return false;
  1400. }
  1401. }
  1402. /**
  1403. * Returns the text label for the specified attribute.
  1404. * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
  1405. * @param string $attribute the attribute name
  1406. * @return string the attribute label
  1407. * @see generateAttributeLabel()
  1408. * @see attributeLabels()
  1409. */
  1410. public function getAttributeLabel($attribute)
  1411. {
  1412. $labels = $this->attributeLabels();
  1413. if (isset($labels[$attribute])) {
  1414. return $labels[$attribute];
  1415. } elseif (strpos($attribute, '.')) {
  1416. $attributeParts = explode('.', $attribute);
  1417. $neededAttribute = array_pop($attributeParts);
  1418. $relatedModel = $this;
  1419. foreach ($attributeParts as $relationName) {
  1420. if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
  1421. $relatedModel = $relatedModel->$relationName;
  1422. } else {
  1423. try {
  1424. $relation = $relatedModel->getRelation($relationName);
  1425. } catch (InvalidParamException $e) {
  1426. return $this->generateAttributeLabel($attribute);
  1427. }
  1428. $relatedModel = new $relation->modelClass;
  1429. }
  1430. }
  1431. $labels = $relatedModel->attributeLabels();
  1432. if (isset($labels[$neededAttribute])) {
  1433. return $labels[$neededAttribute];
  1434. }
  1435. }
  1436. return $this->generateAttributeLabel($attribute);
  1437. }
  1438. /**
  1439. * Returns the text hint for the specified attribute.
  1440. * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
  1441. * @param string $attribute the attribute name
  1442. * @return string the attribute hint
  1443. * @see attributeHints()
  1444. * @since 2.0.4
  1445. */
  1446. public function getAttributeHint($attribute)
  1447. {
  1448. $hints = $this->attributeHints();
  1449. if (isset($hints[$attribute])) {
  1450. return $hints[$attribute];
  1451. } elseif (strpos($attribute, '.')) {
  1452. $attributeParts = explode('.', $attribute);
  1453. $neededAttribute = array_pop($attributeParts);
  1454. $relatedModel = $this;
  1455. foreach ($attributeParts as $relationName) {
  1456. if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
  1457. $relatedModel = $relatedModel->$relationName;
  1458. } else {
  1459. try {
  1460. $relation = $relatedModel->getRelation($relationName);
  1461. } catch (InvalidParamException $e) {
  1462. return '';
  1463. }
  1464. $relatedModel = new $relation->modelClass;
  1465. }
  1466. }
  1467. $hints = $relatedModel->attributeHints();
  1468. if (isset($hints[$neededAttribute])) {
  1469. return $hints[$neededAttribute];
  1470. }
  1471. }
  1472. return '';
  1473. }
  1474. /**
  1475. * @inheritdoc
  1476. *
  1477. * The default implementation returns the names of the columns whose values have been populated into this record.
  1478. */
  1479. public function fields()
  1480. {
  1481. $fields = array_keys($this->_attributes);
  1482. return array_combine($fields, $fields);
  1483. }
  1484. /**
  1485. * @inheritdoc
  1486. *
  1487. * The default implementation returns the names of the relations that have been populated into this record.
  1488. */
  1489. public function extraFields()
  1490. {
  1491. $fields = array_keys($this->getRelatedRecords());
  1492. return array_combine($fields, $fields);
  1493. }
  1494. /**
  1495. * Sets the element value at the specified offset to null.
  1496. * This method is required by the SPL interface [[\ArrayAccess]].
  1497. * It is implicitly called when you use something like `unset($model[$offset])`.
  1498. * @param mixed $offset the offset to unset element
  1499. */
  1500. public function offsetUnset($offset)
  1501. {
  1502. if (property_exists($this, $offset)) {
  1503. $this->$offset = null;
  1504. } else {
  1505. unset($this->$offset);
  1506. }
  1507. }
  1508. }