1
|
|
|
<?php |
2
|
|
|
namespace bedezign\yii2\audit; |
3
|
|
|
|
4
|
|
|
use Yii; |
5
|
|
|
use yii\base\Behavior; |
6
|
|
|
use yii\db\ActiveRecord; |
7
|
|
|
use bedezign\yii2\audit\models\AuditTrail; |
8
|
|
|
use yii\db\Query; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Class AuditTrailBehavior |
12
|
|
|
* @package bedezign\yii2\audit |
13
|
|
|
* |
14
|
|
|
* @property \yii\db\ActiveRecord $owner |
15
|
|
|
*/ |
16
|
|
|
class AuditTrailBehavior extends Behavior |
17
|
|
|
{ |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Array with fields to save |
21
|
|
|
* You don't need to configure both `allowed` and `ignored` |
22
|
|
|
* @var array |
23
|
|
|
*/ |
24
|
|
|
public $allowed = []; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Array with fields to ignore |
28
|
|
|
* You don't need to configure both `allowed` and `ignored` |
29
|
|
|
* @var array |
30
|
|
|
*/ |
31
|
|
|
public $ignored = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Array with classes to ignore |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
public $ignoredClasses = []; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Timestamp attributes should, in most cases, be ignored. |
41
|
|
|
* In case you want to log them, you can set this option to false. |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
public $ignore_timestamps = true; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Is the behavior is active or not |
48
|
|
|
* @var boolean |
49
|
|
|
*/ |
50
|
|
|
public $active = true; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Date format to use in stamp - set to "Y-m-d H:i:s" for datetime or "U" for timestamp |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
public $dateFormat = 'Y-m-d H:i:s'; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var array |
60
|
304 |
|
*/ |
61
|
|
|
private $_oldAttributes = []; |
62
|
|
|
|
63
|
304 |
|
/** |
64
|
304 |
|
* Array with fields you want to override before saving the row into audit_trail table |
65
|
304 |
|
* @var array |
66
|
304 |
|
*/ |
67
|
114 |
|
public $override = []; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @inheritdoc |
71
|
|
|
*/ |
72
|
|
|
public function events() |
73
|
37 |
|
{ |
74
|
|
|
return [ |
75
|
15 |
|
ActiveRecord::EVENT_AFTER_FIND => 'afterFind', |
76
|
37 |
|
ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', |
77
|
|
|
ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate', |
78
|
|
|
ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', |
79
|
|
|
]; |
80
|
|
|
} |
81
|
24 |
|
|
82
|
|
|
/** |
83
|
8 |
|
* |
84
|
8 |
|
*/ |
85
|
24 |
|
public function afterFind() |
86
|
|
|
{ |
87
|
|
|
$this->setOldAttributes($this->owner->getAttributes()); |
88
|
|
|
} |
89
|
|
|
|
90
|
23 |
|
/** |
91
|
|
|
* |
92
|
9 |
|
*/ |
93
|
9 |
|
public function afterInsert() |
94
|
23 |
|
{ |
95
|
|
|
$this->audit('CREATE'); |
96
|
|
|
$this->setOldAttributes($this->owner->getAttributes()); |
97
|
|
|
} |
98
|
|
|
|
99
|
12 |
|
/** |
100
|
|
|
* |
101
|
4 |
|
*/ |
102
|
4 |
|
public function afterUpdate() |
103
|
12 |
|
{ |
104
|
|
|
$this->audit('UPDATE'); |
105
|
|
|
$this->setOldAttributes($this->owner->getAttributes()); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
53 |
|
* |
110
|
|
|
*/ |
111
|
|
|
public function afterDelete() |
112
|
53 |
|
{ |
113
|
9 |
|
$this->audit('DELETE'); |
114
|
|
|
$this->setOldAttributes([]); |
115
|
|
|
} |
116
|
16 |
|
|
117
|
9 |
|
/** |
118
|
|
|
* @param $action |
119
|
|
|
* @throws \yii\db\Exception |
120
|
35 |
|
*/ |
121
|
2 |
|
public function audit($action) |
122
|
6 |
|
{ |
123
|
|
|
// Not active? get out of here |
124
|
|
|
if (!$this->active) { |
125
|
11 |
|
return; |
126
|
29 |
|
} |
127
|
|
|
// Lets check if the whole class should be ignored |
128
|
|
|
if (sizeof($this->ignoredClasses) > 0 && array_search(get_class($this->owner), $this->ignoredClasses) !== false) { |
129
|
|
|
return; |
130
|
|
|
} |
131
|
|
|
// If this is a delete then just write one row and get out of here |
132
|
|
|
if ($action == 'DELETE') { |
133
|
|
|
$this->saveAuditTrailDelete(); |
134
|
11 |
|
return; |
135
|
|
|
} |
136
|
11 |
|
// Now lets actually write the attributes |
137
|
11 |
|
$this->auditAttributes($action); |
138
|
11 |
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Clean attributes of fields that are not allowed or ignored. |
142
|
|
|
* |
143
|
|
|
* @param $attributes |
144
|
|
|
* @return mixed |
145
|
|
|
*/ |
146
|
|
|
protected function cleanAttributes($attributes) |
147
|
11 |
|
{ |
148
|
|
|
$attributes = $this->cleanAttributesAllowed($attributes); |
149
|
11 |
|
$attributes = $this->cleanAttributesIgnored($attributes); |
150
|
2 |
|
$attributes = $this->cleanAttributesOverride($attributes); |
151
|
2 |
|
return $attributes; |
152
|
2 |
|
} |
153
|
2 |
|
|
154
|
2 |
|
/** |
155
|
2 |
|
* Unset attributes which are not allowed |
156
|
11 |
|
* |
157
|
|
|
* @param $attributes |
158
|
|
|
* @return mixed |
159
|
|
|
*/ |
160
|
|
|
protected function cleanAttributesAllowed($attributes) |
161
|
|
|
{ |
162
|
|
|
if (sizeof($this->allowed) > 0) { |
163
|
|
|
foreach ($attributes as $f => $v) { |
164
|
|
|
if (array_search($f, $this->allowed) === false) { |
165
|
11 |
|
unset($attributes[$f]); |
166
|
|
|
} |
167
|
11 |
|
} |
168
|
2 |
|
} |
169
|
2 |
|
return $attributes; |
170
|
2 |
|
} |
171
|
2 |
|
|
172
|
2 |
|
/** |
173
|
2 |
|
* Unset attributes which are ignored |
174
|
11 |
|
* |
175
|
|
|
* @param $attributes |
176
|
|
|
* @return mixed |
177
|
|
|
*/ |
178
|
|
|
protected function cleanAttributesIgnored($attributes) |
179
|
|
|
{ |
180
|
|
|
if($this->ignore_timestamps) |
|
|
|
|
181
|
27 |
|
$this->ignored = array_merge($this->ignored, [ |
182
|
|
|
'created', 'updated', 'created_at', 'updated_at', 'timestamp']); |
183
|
|
|
|
184
|
11 |
|
if (sizeof($this->ignored) > 0) { |
185
|
11 |
|
foreach ($attributes as $f => $v) { |
186
|
|
|
if (array_search($f, $this->ignored) !== false) { |
187
|
11 |
|
unset($attributes[$f]); |
188
|
3 |
|
} |
189
|
|
|
} |
190
|
|
|
} |
191
|
10 |
|
return $attributes; |
192
|
10 |
|
} |
193
|
10 |
|
|
194
|
10 |
|
/** |
195
|
10 |
|
* attributes which need to get override with a new value |
196
|
|
|
* |
197
|
10 |
|
* @param $attributes |
198
|
26 |
|
* @return mixed |
199
|
|
|
*/ |
200
|
|
|
protected function cleanAttributesOverride($attributes) |
201
|
|
|
{ |
202
|
|
|
if (sizeof($this->override) > 0 && sizeof($attributes) >0) { |
203
|
|
|
foreach ($this->override as $field => $queryParams) { |
204
|
|
|
$newOverrideValues = $this->getNewOverrideValues($attributes[$field], $queryParams); |
205
|
|
|
$saveField = \yii\helpers\ArrayHelper::getValue($queryParams, 'saveField', $field); |
206
|
|
|
|
207
|
|
|
if (count($newOverrideValues) >1) { |
208
|
|
|
$attributes[$saveField] = implode(', ', |
209
|
|
|
\yii\helpers\ArrayHelper::map($newOverrideValues, $queryParams['returnField'], $queryParams['returnField']) |
210
|
|
|
); |
211
|
|
|
} elseif (count($newOverrideValues) == 1) { |
212
|
|
|
$attributes[$saveField] = $newOverrideValues[0][$queryParams['returnField']]; |
213
|
26 |
|
} |
214
|
|
|
} |
215
|
|
|
} |
216
|
26 |
|
return $attributes; |
217
|
10 |
|
} |
218
|
14 |
|
|
219
|
|
|
/** |
220
|
14 |
|
* @param string $searchFieldValue |
221
|
16 |
|
* @param string $queryParams |
222
|
10 |
|
* @return mixed |
223
|
10 |
|
*/ |
224
|
|
|
private function getNewOverrideValues($searchFieldValue, $queryParams) |
225
|
26 |
|
{ |
226
|
26 |
|
$query = new Query; |
227
|
10 |
|
|
228
|
10 |
|
$query->select($queryParams['returnField']) |
229
|
10 |
|
->from($queryParams['tableName']) |
230
|
26 |
|
->where([$queryParams['searchField'] => $searchFieldValue]); |
231
|
|
|
|
232
|
|
|
$rows = $query->all(); |
233
|
|
|
|
234
|
|
|
return $rows; |
235
|
6 |
|
} |
236
|
|
|
|
237
|
2 |
|
|
238
|
2 |
|
/** |
239
|
6 |
|
* @param string $action |
240
|
6 |
|
* @throws \yii\db\Exception |
241
|
6 |
|
*/ |
242
|
6 |
|
protected function auditAttributes($action) |
243
|
6 |
|
{ |
244
|
6 |
|
// Get the new and old attributes |
245
|
2 |
|
$newAttributes = $this->cleanAttributes($this->owner->getAttributes()); |
246
|
6 |
|
$oldAttributes = $this->cleanAttributes($this->getOldAttributes()); |
247
|
|
|
// If no difference then get out of here |
248
|
|
|
if (count(array_diff_assoc($newAttributes, $oldAttributes)) <= 0) { |
249
|
|
|
return; |
250
|
|
|
} |
251
|
29 |
|
// Get the trail data |
252
|
|
|
$entry_id = $this->getAuditEntryId(); |
253
|
29 |
|
$user_id = $this->getUserId(); |
254
|
|
|
$model = $this->owner->className(); |
255
|
|
|
$model_id = $this->getNormalizedPk(); |
256
|
|
|
$created = date($this->dateFormat); |
257
|
|
|
|
258
|
|
|
$this->saveAuditTrail($action, $newAttributes, $oldAttributes, $entry_id, $user_id, $model, $model_id, $created); |
259
|
27 |
|
} |
260
|
|
|
|
261
|
27 |
|
/** |
262
|
27 |
|
* Save the audit trails for a create or update action |
263
|
|
|
* |
264
|
|
|
* @param $action |
265
|
|
|
* @param $newAttributes |
266
|
|
|
* @param $oldAttributes |
267
|
12 |
|
* @param $entry_id |
268
|
|
|
* @param $user_id |
269
|
12 |
|
* @param $model |
270
|
12 |
|
* @param $model_id |
271
|
|
|
* @param $created |
272
|
|
|
* @throws \yii\db\Exception |
273
|
|
|
*/ |
274
|
|
|
protected function saveAuditTrail($action, $newAttributes, $oldAttributes, $entry_id, $user_id, $model, $model_id, $created) |
275
|
|
|
{ |
276
|
12 |
|
// Build a list of fields to log |
277
|
|
|
$rows = array(); |
278
|
12 |
|
foreach ($newAttributes as $field => $new) { |
279
|
|
|
$old = isset($oldAttributes[$field]) ? $oldAttributes[$field] : ''; |
280
|
|
|
// If they are not the same lets write an audit log |
281
|
|
|
if ($new != $old) { |
282
|
|
|
$rows[] = [$entry_id, $user_id, $old, $new, $action, $model, $model_id, $field, $created]; |
283
|
|
|
} |
284
|
|
|
} |
285
|
32 |
|
// Record the field changes with a batch insert |
286
|
|
|
if (!empty($rows)) { |
287
|
12 |
|
$columns = ['entry_id', 'user_id', 'old_value', 'new_value', 'action', 'model', 'model_id', 'field', 'created']; |
288
|
32 |
|
$audit = Audit::getInstance(); |
289
|
|
|
$audit->getDb()->createCommand()->batchInsert(AuditTrail::tableName(), $columns, $rows)->execute(); |
290
|
|
|
} |
291
|
32 |
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
12 |
|
* Save the audit trails for a delete action |
295
|
|
|
*/ |
296
|
|
|
protected function saveAuditTrailDelete() |
297
|
|
|
{ |
298
|
|
|
$audit = Audit::getInstance(); |
299
|
|
|
$audit->getDb()->createCommand()->insert(AuditTrail::tableName(), [ |
300
|
|
|
'action' => 'DELETE', |
301
|
|
|
'entry_id' => $this->getAuditEntryId(), |
302
|
|
|
'user_id' => $this->getUserId(), |
303
|
|
|
'model' => $this->owner->className(), |
304
|
|
|
'model_id' => $this->getNormalizedPk(), |
305
|
|
|
'created' => date($this->dateFormat), |
306
|
|
|
])->execute(); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @return array |
311
|
|
|
*/ |
312
|
|
|
public function getOldAttributes() |
313
|
|
|
{ |
314
|
|
|
return $this->_oldAttributes; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @param $value |
319
|
|
|
*/ |
320
|
|
|
public function setOldAttributes($value) |
321
|
|
|
{ |
322
|
|
|
$this->_oldAttributes = $value; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* @return string |
327
|
|
|
*/ |
328
|
|
|
protected function getNormalizedPk() |
329
|
|
|
{ |
330
|
|
|
$pk = $this->owner->getPrimaryKey(); |
331
|
|
|
return is_array($pk) ? json_encode($pk) : $pk; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* @return int|null|string |
336
|
|
|
*/ |
337
|
|
|
protected function getUserId() |
338
|
|
|
{ |
339
|
|
|
return Audit::getInstance()->getUserId(); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* @return models\AuditEntry|null|static |
344
|
|
|
* @throws \Exception |
345
|
|
|
*/ |
346
|
|
|
protected function getAuditEntryId() |
347
|
|
|
{ |
348
|
|
|
$module = Audit::getInstance(); |
349
|
|
|
if (!$module) { |
350
|
|
|
$module = \Yii::$app->getModule(Audit::findModuleIdentifier()); |
351
|
|
|
} |
352
|
|
|
if (!$module) { |
353
|
|
|
throw new \Exception('Audit module cannot be loaded'); |
354
|
|
|
} |
355
|
|
|
return Audit::getInstance()->getEntry(true)->id; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
} |
359
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.