Completed
Push — master ( cd0e0e...9ba558 )
by Carsten
13:34
created

OptimisticLockBehavior   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 106
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 91.89%

Importance

Changes 0
Metric Value
wmc 17
lcom 1
cbo 6
dl 0
loc 106
ccs 34
cts 37
cp 0.9189
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A attach() 0 9 2
A events() 0 8 2
A getLockAttribute() 0 15 4
A getValue() 0 14 6
A upgrade() 0 11 3
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
8
namespace yii\behaviors;
9
10
use Yii;
11
use yii\db\BaseActiveRecord;
12
use yii\base\InvalidCallException;
13
use yii\validators\NumberValidator;
14
use yii\helpers\ArrayHelper;
15
16
/**
17
 * OptimisticLockBehavior automatically upgrades a model's lock version using the column name 
18
 * returned by [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
19
 *
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.
24
 * 
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:
29
 *
30
 * ```php
31
 * use yii\behaviors\OptimisticLockBehavior;
32
 *
33
 * public function behaviors()
34
 * {
35
 *     return [
36
 *         OptimisticLockBehavior::className(),
37
 *     ];
38
 * }
39
 * ```
40
 *
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. 
45
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.
52
 * 
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:
56
 *
57
 * ```php
58
 * $model->upgrade();
59
 * ```
60
 *
61
 * @author Salem Ouerdani <[email protected]>
62
 * @since 2.0.16
63
 * @see \yii\db\BaseActiveRecord::optimisticLock() for details on how to enable optimistic lock.
64
 */
65
class OptimisticLockBehavior extends AttributeBehavior
66
{
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * In case of `null` value it will be directly parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
71
     */
72
    public $value;
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public $skipUpdateOnClean = false;
77
    /**
78
     * @var string the attribute name holding the version value.
79
     */
80
    private $_lockAttribute;
81
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 4
    public function attach($owner)
87
    {
88 4
        parent::attach($owner);
89
90 4
        if (empty($this->attributes)) {
91 4
            $lock = $this->getLockAttribute();
92 4
            $this->attributes = array_fill_keys(array_keys($this->events()), $lock);
93
        }
94 4
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 4
    public function events()
100
    {
101 4
        return Yii::$app->request instanceof \yii\web\Request ? [
102 3
            BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
103
            BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
104
            BaseActiveRecord::EVENT_BEFORE_DELETE => 'evaluateAttributes',
105 4
        ] : [];
106
    }
107
108
    /**
109
     * Returns the column name to hold the version value as defined in [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
110
     * @return string the property name.
111
     * @throws InvalidCallException if [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]] is not properly configured.
112
     * @since 2.0.16
113
     */
114 4
    protected function getLockAttribute()
115
    {
116 4
        if ($this->_lockAttribute) {
117 4
            return $this->_lockAttribute;
118
        }
119
120
        /* @var $owner BaseActiveRecord */
121 4
        $owner = $this->owner;
122 4
        $lock = $owner->optimisticLock();
123 4
        if ($lock === null || $owner->hasAttribute($lock) === false) {
124
            throw new InvalidCallException("Unable to get the optimistic lock attribute. Probably 'optimisticLock()' method is misconfigured.");
125
        }
126 4
        $this->_lockAttribute = $lock;
127 4
        return $lock;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     *
133
     * In case of `null`, value will be parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
134
     */
135 3
    protected function getValue($event)
136
    {
137 3
        if ($this->value === null) {
138 3
            $request = Yii::$app->getRequest();
139 3
            $lock = $this->getLockAttribute();
140 3
            $formName = $this->owner->formName();
0 ignored issues
show
Documentation Bug introduced by
The method formName does not exist on object<yii\base\Component>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
141 3
            $formValue = $formName ? ArrayHelper::getValue($request->getBodyParams(), $formName . '.' . $lock) : null;
142 3
            $input = $formValue ?: $request->getBodyParam($lock);
143 3
            $isValid = $input && (new NumberValidator())->validate($input);
144 3
            return $isValid ? $input : 0;
145
        }
146
147
        return parent::getValue($event);
148
    }
149
150
    /**
151
     * Upgrades the version value by one and stores it to database.
152
     *
153
     * ```php
154
     * $model->upgrade();
155
     * ```
156
     * @throws InvalidCallException if owner is a new record.
157
     * @since 2.0.16
158
     */
159 3
    public function upgrade()
160
    {
161
        /* @var $owner BaseActiveRecord */
162 3
        $owner = $this->owner;
163 3
        if ($owner->getIsNewRecord()) {
164
            throw new InvalidCallException('Upgrading the model version is not possible on a new record.');
165
        }
166 3
        $lock = $this->getLockAttribute();
167 3
        $version = $owner->$lock ?: 0;
168 3
        $owner->updateAttributes([$lock => $version + 1]);
169 3
    }
170
}
171