AuditTrailBehavior::cleanAttributesOverride()   A
last analyzed

Complexity

Conditions 6
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 31.2848

Importance

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