3
* @link https://www.yiiframework.com/
4
* @copyright Copyright (c) 2008 Yii Software LLC
5
* @license https://www.yiiframework.com/license/
8
namespace yii\behaviors;
11
use yii\db\BaseActiveRecord;
12
use yii\base\InvalidCallException;
13
use yii\validators\NumberValidator;
14
use yii\helpers\ArrayHelper;
17
* OptimisticLockBehavior automatically upgrades a model's lock version using the column name
18
* returned by [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
20
* Optimistic locking allows multiple users to access the same record for edits and avoids
21
* potential conflicts. In case when a user attempts to save the record upon some staled data
22
* (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
23
* and the update or deletion is skipped.
25
* To use this behavior, first enable optimistic lock by following the steps listed in
26
* [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]], remove the column name
27
* holding the lock version from the [[\yii\base\Model::rules()|rules()]] method of your
28
* ActiveRecord class, then add the following code to it:
31
* use yii\behaviors\OptimisticLockBehavior;
33
* public function behaviors()
36
* OptimisticLockBehavior::class,
41
* By default, OptimisticLockBehavior will use [[\yii\web\Request::getBodyParam()|getBodyParam()]] to parse
42
* the submitted value or set it to 0 on any fail. That means a request not holding the version attribute
43
* may achieve a first successful update to entity, but starting from there any further try should fail
44
* unless the request is holding the expected version number.
46
* Once attached, internal use of the model class should also fail to save the record if the version number
47
* isn't held by [[\yii\web\Request::getBodyParam()|getBodyParam()]]. It may be useful to extend your model class,
48
* enable optimistic lock in parent class by overriding [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]],
49
* then attach the behavior to the child class so you can tie the parent model to internal use while linking the child model
50
* holding this behavior to the controllers responsible of receiving end user inputs.
51
* Alternatively, you can also configure the [[value]] property with a PHP callable to implement a different logic.
53
* OptimisticLockBehavior also provides a method named [[upgrade()]] that increases a model's
54
* version by one, that may be useful when you need to mark an entity as stale among connected clients
55
* and avoid any change to it until they load it again:
61
* @author Salem Ouerdani <tunecino@gmail.com>
63
* @see \yii\db\BaseActiveRecord::optimisticLock() for details on how to enable optimistic lock.
65
class OptimisticLockBehavior extends AttributeBehavior
70
* In case of `null` value it will be directly parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
76
public $skipUpdateOnClean = false;
79
* @var string the attribute name holding the version value.
81
private $_lockAttribute;
87
public function attach($owner)
89
parent::attach($owner);
91
if (empty($this->attributes)) {
92
$lock = $this->getLockAttribute();
93
$this->attributes = array_fill_keys(array_keys($this->events()), $lock);
100
public function events()
102
return Yii::$app->request instanceof \yii\web\Request ? [
103
BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
104
BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
105
BaseActiveRecord::EVENT_BEFORE_DELETE => 'evaluateAttributes',
110
* Returns the column name to hold the version value as defined in [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
111
* @return string the property name.
112
* @throws InvalidCallException if [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]] is not properly configured.
115
protected function getLockAttribute()
117
if ($this->_lockAttribute) {
118
return $this->_lockAttribute;
121
/* @var $owner BaseActiveRecord */
122
$owner = $this->owner;
123
$lock = $owner->optimisticLock();
124
if ($lock === null || $owner->hasAttribute($lock) === false) {
125
throw new InvalidCallException("Unable to get the optimistic lock attribute. Probably 'optimisticLock()' method is misconfigured.");
127
$this->_lockAttribute = $lock;
134
* In case of `null`, value will be parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
136
protected function getValue($event)
138
if ($this->value === null) {
139
$request = Yii::$app->getRequest();
140
$lock = $this->getLockAttribute();
141
$formName = $this->owner->formName();
142
$formValue = $formName ? ArrayHelper::getValue($request->getBodyParams(), $formName . '.' . $lock) : null;
143
$input = $formValue ?: $request->getBodyParam($lock);
144
$isValid = $input && (new NumberValidator())->validate($input);
145
return $isValid ? $input : 0;
148
return parent::getValue($event);
152
* Upgrades the version value by one and stores it to database.
157
* @throws InvalidCallException if owner is a new record.
160
public function upgrade()
162
/* @var $owner BaseActiveRecord */
163
$owner = $this->owner;
164
if ($owner->getIsNewRecord()) {
165
throw new InvalidCallException('Upgrading the model version is not possible on a new record.');
167
$lock = $this->getLockAttribute();
168
$version = $owner->$lock ?: 0;
169
$owner->updateAttributes([$lock => $version + 1]);