Completed
Push — master ( cc2cac...1945dc )
by Alexander
13:23
created

OptimisticLockBehavior::attach()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
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
15
/**
16
 * OptimisticLockBehavior automatically upgrades a model's lock version using the column name 
17
 * returned by [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
18
 *
19
 * Optimistic locking allows multiple users to access the same record for edits and avoids
20
 * potential conflicts. In case when a user attempts to save the record upon some staled data
21
 * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
22
 * and the update or deletion is skipped.
23
 * 
24
 * To use this behavior, first enable optimistic lock by following the steps listed in 
25
 * [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]], remove the column name 
26
 * holding the lock version from the [[\yii\base\Model::rules()|rules()]] method of your 
27
 * ActiveRecord class, then add the following code to it:
28
 *
29
 * ```php
30
 * use yii\behaviors\OptimisticLockBehavior;
31
 *
32
 * public function behaviors()
33
 * {
34
 *     return [
35
 *         OptimisticLockBehavior::className(),
36
 *     ];
37
 * }
38
 * ```
39
 *
40
 * By default, OptimisticLockBehavior will use [[\yii\web\Request::getBodyParam()|getBodyParam()]] to parse
41
 * the submitted value or set it to 0 on any fail. That means a request not holding the version attribute
42
 * may achieve a first successful update to entity, but starting from there any further try should fail
43
 * unless the request is holding the expected version number. You can also configure the [[value]] property 
44
 * with a PHP callable to implement a different logic.
45
 * 
46
 * OptimisticLockBehavior also provides a method named [[upgrade()]] that increases a model's 
47
 * version by one, that may be useful when you need to mark an entity as stale among connected clients
48
 * and avoid any change to it until they load it again:
49
 *
50
 * ```php
51
 * $model->upgrade();
52
 * ```
53
 *
54
 * @author Salem Ouerdani <[email protected]>
55
 * @since 2.0.16
56
 * @see \yii\db\BaseActiveRecord::optimisticLock() for details on how to enable optimistic lock.
57
 */
58
class OptimisticLockBehavior extends AttributeBehavior
59
{
60
    /**
61
     * {@inheritdoc}
62
     *
63
     * In case of `null` value it will be directly parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
64
     */
65
    public $value;
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public $skipUpdateOnClean = false;
70
    /**
71
     * @var string the attribute name holding the version value.
72
     */
73
    private $_lockAttribute;
74
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 4
    public function attach($owner)
80
    {
81 4
        parent::attach($owner);
82
83 4
        if (empty($this->attributes)) {
84 4
            $lock = $this->getLockAttribute();
85 4
            $this->attributes = array_fill_keys(array_keys($this->events()), $lock);
86
        }
87 4
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 4
    public function events()
93
    {
94 4
        return Yii::$app->request instanceof \yii\web\Request ? [
95 3
            BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
96
            BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
97
            BaseActiveRecord::EVENT_BEFORE_DELETE => 'evaluateAttributes',
98 4
        ] : [];
99
    }
100
101
    /**
102
     * Returns the column name to hold the version value as defined in [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]].
103
     * @return string the property name.
104
     * @throws InvalidCallException if [[\yii\db\BaseActiveRecord::optimisticLock()|optimisticLock()]] is not properly configured.
105
     * @since 2.0.16
106
     */
107 4
    protected function getLockAttribute()
108
    {
109 4
        if ($this->_lockAttribute) {
110 4
            return $this->_lockAttribute;
111
        }
112
113
        /* @var $owner BaseActiveRecord */
114 4
        $owner = $this->owner;
115 4
        $lock = $owner->optimisticLock();
116 4
        if ($lock === null || $owner->hasAttribute($lock) === false) {
117
            throw new InvalidCallException("Unable to get the optimistic lock attribute. Probably 'optimisticLock()' method is misconfigured.");
118
        }
119 4
        $this->_lockAttribute = $lock;
120 4
        return $lock;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     *
126
     * In case of `null`, value will be parsed from [[\yii\web\Request::getBodyParam()|getBodyParam()]] or set to 0.
127
     */
128 3
    protected function getValue($event)
129
    {
130 3
        if ($this->value === null) {
131 3
            $lock = $this->getLockAttribute();
132 3
            $input = Yii::$app->getRequest()->getBodyParam($lock);
133 3
            $isValid = $input && (new NumberValidator())->validate($input);
134 3
            return $isValid ? $input : 0;
135
        }
136
137
        return parent::getValue($event);
138
    }
139
140
    /**
141
     * Upgrades the version value by one and stores it to database.
142
     *
143
     * ```php
144
     * $model->upgrade();
145
     * ```
146
     * @throws InvalidCallException if owner is a new record.
147
     * @since 2.0.16
148
     */
149 3
    public function upgrade()
150
    {
151
        /* @var $owner BaseActiveRecord */
152 3
        $owner = $this->owner;
153 3
        if ($owner->getIsNewRecord()) {
154
            throw new InvalidCallException('Upgrading the model version is not possible on a new record.');
155
        }
156 3
        $lock = $this->getLockAttribute();
157 3
        $version = $owner->$lock ?: 0;
158 3
        $owner->updateAttributes([$lock => $version + 1]);
159 3
    }
160
}
161