AttributesBehavior.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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\behaviors;
  8. use Closure;
  9. use yii\base\Behavior;
  10. use yii\base\Event;
  11. use yii\db\ActiveRecord;
  12. /**
  13. * AttributesBehavior automatically assigns values specified to one or multiple attributes of an ActiveRecord
  14. * object when certain events happen.
  15. *
  16. * To use AttributesBehavior, configure the [[attributes]] property which should specify the list of attributes
  17. * that need to be updated and the corresponding events that should trigger the update. Then configure the
  18. * value of enclosed arrays with a PHP callable whose return value will be used to assign to the current attribute.
  19. * For example,
  20. *
  21. * ```php
  22. * use yii\behaviors\AttributesBehavior;
  23. *
  24. * public function behaviors()
  25. * {
  26. * return [
  27. * [
  28. * 'class' => AttributesBehavior::className(),
  29. * 'attributes' => [
  30. * 'attribute1' => [
  31. * ActiveRecord::EVENT_BEFORE_INSERT => new Expression('NOW()'),
  32. * ActiveRecord::EVENT_BEFORE_UPDATE => \Yii::$app->formatter->asDatetime('2017-07-13'),
  33. * ],
  34. * 'attribute2' => [
  35. * ActiveRecord::EVENT_BEFORE_VALIDATE => [$this, 'storeAttributes'],
  36. * ActiveRecord::EVENT_AFTER_VALIDATE => [$this, 'restoreAttributes'],
  37. * ],
  38. * 'attribute3' => [
  39. * ActiveRecord::EVENT_BEFORE_VALIDATE => $fn2 = [$this, 'getAttribute2'],
  40. * ActiveRecord::EVENT_AFTER_VALIDATE => $fn2,
  41. * ],
  42. * 'attribute4' => [
  43. * ActiveRecord::EVENT_BEFORE_DELETE => function ($event, $attribute) {
  44. * static::disabled() || $event->isValid = false;
  45. * },
  46. * ],
  47. * ],
  48. * ],
  49. * ];
  50. * }
  51. * ```
  52. *
  53. * Because attribute values will be set automatically by this behavior, they are usually not user input and should therefore
  54. * not be validated, i.e. they should not appear in the [[\yii\base\Model::rules()|rules()]] method of the model.
  55. *
  56. * @author Luciano Baraglia <luciano.baraglia@gmail.com>
  57. * @author Qiang Xue <qiang.xue@gmail.com>
  58. * @author Bogdan Stepanenko <bscheshirwork@gmail.com>
  59. * @since 2.0.13
  60. */
  61. class AttributesBehavior extends Behavior
  62. {
  63. /**
  64. * @var array list of attributes that are to be automatically filled with the values specified via enclosed arrays.
  65. * The array keys are the ActiveRecord attributes upon which the events are to be updated,
  66. * and the array values are the array of corresponding events(s). For this enclosed array:
  67. * the array keys are the ActiveRecord events upon which the attributes are to be updated,
  68. * and the array values are the value that will be assigned to the current attributes. This can be an anonymous function,
  69. * callable in array format (e.g. `[$this, 'methodName']`), an [[\yii\db\Expression|Expression]] object representing a DB expression
  70. * (e.g. `new Expression('NOW()')`), scalar, string or an arbitrary value. If the former, the return value of the
  71. * function will be assigned to the attributes.
  72. *
  73. * ```php
  74. * [
  75. * 'attribute1' => [
  76. * ActiveRecord::EVENT_BEFORE_INSERT => new Expression('NOW()'),
  77. * ActiveRecord::EVENT_BEFORE_UPDATE => \Yii::$app->formatter->asDatetime('2017-07-13'),
  78. * ],
  79. * 'attribute2' => [
  80. * ActiveRecord::EVENT_BEFORE_VALIDATE => [$this, 'storeAttributes'],
  81. * ActiveRecord::EVENT_AFTER_VALIDATE => [$this, 'restoreAttributes'],
  82. * ],
  83. * 'attribute3' => [
  84. * ActiveRecord::EVENT_BEFORE_VALIDATE => $fn2 = [$this, 'getAttribute2'],
  85. * ActiveRecord::EVENT_AFTER_VALIDATE => $fn2,
  86. * ],
  87. * 'attribute4' => [
  88. * ActiveRecord::EVENT_BEFORE_DELETE => function ($event, $attribute) {
  89. * static::disabled() || $event->isValid = false;
  90. * },
  91. * ],
  92. * ]
  93. * ```
  94. */
  95. public $attributes = [];
  96. /**
  97. * @var array list of order of attributes that are to be automatically filled with the event.
  98. * The array keys are the ActiveRecord events upon which the attributes are to be updated,
  99. * and the array values are represent the order corresponding attributes.
  100. * The rest of the attributes are processed at the end.
  101. * If the [[attributes]] for this attribute do not specify this event, it is ignored
  102. *
  103. * ```php
  104. * [
  105. * ActiveRecord::EVENT_BEFORE_VALIDATE => ['attribute1', 'attribute2'],
  106. * ActiveRecord::EVENT_AFTER_VALIDATE => ['attribute2', 'attribute1'],
  107. * ]
  108. * ```
  109. */
  110. public $order = [];
  111. /**
  112. * @var bool whether to skip this behavior when the `$owner` has not been modified
  113. */
  114. public $skipUpdateOnClean = true;
  115. /**
  116. * @var bool whether to preserve non-empty attribute values.
  117. */
  118. public $preserveNonEmptyValues = false;
  119. /**
  120. * {@inheritdoc}
  121. */
  122. public function events()
  123. {
  124. return array_fill_keys(
  125. array_reduce($this->attributes, function ($carry, $item) {
  126. return array_merge($carry, array_keys($item));
  127. }, []),
  128. 'evaluateAttributes'
  129. );
  130. }
  131. /**
  132. * Evaluates the attributes values and assigns it to the current attributes.
  133. * @param Event $event
  134. */
  135. public function evaluateAttributes($event)
  136. {
  137. if ($this->skipUpdateOnClean
  138. && $event->name === ActiveRecord::EVENT_BEFORE_UPDATE
  139. && empty($this->owner->dirtyAttributes)
  140. ) {
  141. return;
  142. }
  143. $attributes = array_keys(array_filter($this->attributes, function ($carry) use ($event) {
  144. return array_key_exists($event->name, $carry);
  145. }));
  146. if (!empty($this->order[$event->name])) {
  147. $attributes = array_merge(
  148. array_intersect((array) $this->order[$event->name], $attributes),
  149. array_diff($attributes, (array) $this->order[$event->name]));
  150. }
  151. foreach ($attributes as $attribute) {
  152. if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) {
  153. continue;
  154. }
  155. $this->owner->$attribute = $this->getValue($attribute, $event);
  156. }
  157. }
  158. /**
  159. * Returns the value for the current attributes.
  160. * This method is called by [[evaluateAttributes()]]. Its return value will be assigned
  161. * to the target attribute corresponding to the triggering event.
  162. * @param string $attribute target attribute name
  163. * @param Event $event the event that triggers the current attribute updating.
  164. * @return mixed the attribute value
  165. */
  166. protected function getValue($attribute, $event)
  167. {
  168. if (!isset($this->attributes[$attribute][$event->name])) {
  169. return null;
  170. }
  171. $value = $this->attributes[$attribute][$event->name];
  172. if ($value instanceof Closure || (is_array($value) && is_callable($value))) {
  173. return $value($event, $attribute);
  174. }
  175. return $value;
  176. }
  177. }