1+ <?php
2+
3+ namespace Reshadman \OptimisticLocking ;
4+
5+ use Illuminate \Database \Eloquent \Builder ;
6+ use Illuminate \Database \Eloquent \Model ;
7+
8+ trait OptimisticLocking
9+ {
10+ /**
11+ * Indicates that models uses locking or not?
12+ *
13+ * @var bool
14+ */
15+ protected $ lock = true ;
16+
17+ /**
18+ * Hooks model events to add lock version if not set.
19+ *
20+ * @return void
21+ */
22+ protected static function boot ()
23+ {
24+ static ::creating (function (Model $ model ) {
25+
26+ if ($ model ->currentLockVersion () === null ) {
27+
28+ $ model ->{static ::lockVersionColumn ()} = static ::defaultLockVersion ();
29+
30+ }
31+
32+ return $ model ;
33+ });
34+
35+ parent ::boot ();
36+ }
37+
38+ /**
39+ * Perform a model update operation respecting optimistic locking.
40+ * If the lock fails it will throw a "StaleModelLockingException"
41+ *
42+ * @param Builder $query
43+ * @return bool
44+ */
45+ protected function performUpdate (Builder $ query )
46+ {
47+ // If the updating event returns false, we will cancel the update operation so
48+ // developers can hook Validation systems into their models and cancel this
49+ // operation if the model does not pass validation. Otherwise, we update.
50+ if ($ this ->fireModelEvent ('updating ' ) === false ) {
51+ return false ;
52+ }
53+
54+ // First we need to create a fresh query instance and touch the creation and
55+ // update timestamp on the model which are maintained by us for developer
56+ // convenience. Then we will just continue saving the model instances.
57+ if ($ this ->usesTimestamps ()) {
58+ $ this ->updateTimestamps ();
59+ }
60+
61+ // Once we have run the update operation, we will fire the "updated" event for
62+ // this model instance. This will allow developers to hook into these after
63+ // models are updated, giving them a chance to do any special processing.
64+ $ dirty = $ this ->getDirty ();
65+
66+ if (count ($ dirty ) > 0 ) {
67+
68+ $ versionColumn = static ::lockVersionColumn ();
69+
70+ $ this ->setKeysForSaveQuery ($ query );
71+
72+ // If model locking is enabled, the lock version check constraint is
73+ // added to the update query, as every update on the model increments the version
74+ // by exactly "1" we will increment the value by one for update, then.
75+ if ($ this ->lockingEnabled ()) {
76+ $ query ->where ($ versionColumn , '= ' , $ this ->currentLockVersion ());
77+ }
78+
79+ $ beforeUpdateVersion = $ this ->currentLockVersion ();
80+
81+ $ this ->setAttribute ($ versionColumn , $ newVersion = $ beforeUpdateVersion + 1 );
82+ $ dirty [$ versionColumn ] = $ newVersion ;
83+
84+ // If there is no record affected by our update query,
85+ // It means that the record has been updated by another process,
86+ // Or has been deleted, as we treat "delete" as an act of update
87+ // we throw the exception in this situation anyway.
88+ $ affected = $ query ->update ($ dirty );
89+
90+ if ($ affected === 0 ) {
91+ $ this ->setAttribute ($ versionColumn , $ beforeUpdateVersion );
92+
93+ throw new StaleModelLockingException ("Model has been changed during update. " );
94+ }
95+
96+ $ this ->fireModelEvent ('updated ' , false );
97+
98+ $ this ->syncChanges ();
99+ }
100+
101+ return true ;
102+ }
103+
104+ /**
105+ * Name of the lock version column.
106+ *
107+ * @return string
108+ */
109+ protected static function lockVersionColumn ()
110+ {
111+ return 'lock_version ' ;
112+ }
113+
114+ /**
115+ * Current lock version value.
116+ *
117+ * @return int
118+ */
119+ public function currentLockVersion ()
120+ {
121+ return $ this ->getAttribute (static ::lockVersionColumn ());
122+ }
123+
124+ /**
125+ * Default lock version value.
126+ *
127+ * @return int
128+ */
129+ protected static function defaultLockVersion ()
130+ {
131+ return 1 ;
132+ }
133+
134+ /**
135+ * Indicates that optimistic locking is enabled for this model
136+ * instance or not.
137+ *
138+ * @return bool
139+ */
140+ protected function lockingEnabled ()
141+ {
142+ return $ this ->lock === null ? true : $ this ->lock ;
143+ }
144+
145+ /**
146+ * Disables optimistic locking for this model instance.
147+ *
148+ * @return $this
149+ */
150+ protected function disableLocking ()
151+ {
152+ $ this ->lock = false ;
153+ return $ this ;
154+ }
155+
156+ /**
157+ * Enables optimistic locking for this model instance.
158+ *
159+ * @return $this
160+ */
161+ public function enableLocking ()
162+ {
163+ $ this ->lock = true ;
164+ return $ this ;
165+ }
166+ }
0 commit comments