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.

BaseActiveRecord.php 63KB

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