Passed
Push — master ( ed3719...9cc54a )
by Loban
02:45
created

ActiveLogBehavior   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Test Coverage

Coverage 98.04%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 58
eloc 145
dl 0
loc 365
c 7
b 0
f 0
ccs 150
cts 153
cp 0.9804
rs 4.5599

20 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeSave() 0 4 2
A addLog() 0 12 1
A isEmpty() 0 6 4
A resolveStoreValues() 0 10 3
A beforeSaveMessage() 0 13 3
A __construct() 0 7 1
A resolveListValues() 0 22 6
A saveMessage() 0 5 1
A events() 0 11 2
A getEntityName() 0 12 3
A beforeDelete() 0 9 2
A resolveSimpleValues() 0 11 5
B prepareChangedAttributes() 0 16 7
A init() 0 3 1
A initAttributes() 0 6 3
A getEntityId() 0 17 5
A afterSaveMessage() 0 7 2
A resolveRelationValues() 0 22 2
A afterSave() 0 6 2
A filterStoreValues() 0 6 3

How to fix   Complexity   

Complex Class

Complex classes like ActiveLogBehavior often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActiveLogBehavior, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://github.com/lav45/yii2-activity-logger
4
 * @copyright Copyright (c) 2017 LAV45
5
 * @author Aleksey Loban <[email protected]>
6
 * @license http://opensource.org/licenses/BSD-3-Clause
7
 */
8
9
namespace lav45\activityLogger;
10
11
use lav45\activityLogger\storage\DeleteCommand;
12
use lav45\activityLogger\storage\MessageData;
13
use Yii;
14
use yii\base\Behavior;
15
use yii\base\InvalidValueException;
16
use yii\db\ActiveRecord;
17
use yii\db\ArrayExpression;
18
use yii\db\JsonExpression;
19
use yii\helpers\ArrayHelper;
20
use yii\helpers\Inflector;
21
use yii\helpers\StringHelper;
22
23
/**
24
 * ======================= Example usage ======================
25
 *
26
 *  // Recommended
27
 *  public function transactions()
28
 *  {
29
 *      return [
30
 *          ActiveRecord::SCENARIO_DEFAULT => ActiveRecord::OP_ALL,
31
 *      ];
32
 *  }
33
 *
34
 *  public function behaviors()
35
 *  {
36
 *      return [
37
 *          [
38
 *              '__class' => 'lav45\activityLogger\ActiveLogBehavior',
39
 *              'attributes' => [
40
 *                  // simple attribute
41
 *                  'title',
42
 *
43
 *                  // the value of the attribute is a item in the list
44
 *                  'status' => [
45
 *                      // => $this->getStatusList()
46
 *                      'list' => 'statusList'
47
 *                  ],
48
 *
49
 *                  // the attribute value is the [id] of the relation model
50
 *                  'owner_id' => [
51
 *                      'relation' => 'owner',
52
 *                      'attribute' => 'username',
53
 *                  ],
54
 *              ]
55
 *          ]
56
 *      ];
57
 *  }
58
 * ============================================================
59
 *
60
 * @property string $entityName
61
 * @property string $entityId
62
 * @property ActiveRecord $owner
63
 */
64
class ActiveLogBehavior extends Behavior
65
{
66
    /**
67
     * @event MessageEvent an event that is triggered before inserting a record.
68
     * You may add in to the [[MessageEvent::append]] your custom log message.
69
     * @since 1.5.3
70
     */
71
    public const EVENT_BEFORE_SAVE_MESSAGE = 'beforeSaveMessage';
72
    /**
73
     * @event Event an event that is triggered after inserting a record.
74
     * @since 1.5.3
75
     */
76
    public const EVENT_AFTER_SAVE_MESSAGE = 'afterSaveMessage';
77
78
    public bool $softDelete = false;
79
    /** @since 1.6.0 */
80
    public ?\Closure $beforeSaveMessage = null;
81
    /**
82
     * [
83
     *  // simple attribute
84
     *  'title',
85
     *
86
     *  // simple boolean attribute
87
     *  'is_publish',
88
     *
89
     *  // the value of the attribute is a item in the list
90
     *  // => $this->getStatusList()
91
     *  'status' => [
92
     *      'list' => 'statusList'
93
     *  ],
94
     *
95
     *  // the attribute value is the [id] of the relation model
96
     *  'owner_id' => [
97
     *      'relation' => 'user',
98
     *      'attribute' => 'username'
99
     *  ]
100
     * ]
101
     */
102
    public array $attributes = [];
103
104
    public bool $identicalAttributes = false;
105
    /**
106
     * A PHP callable that replaces the default implementation of [[isEmpty()]].
107
     * @since 1.5.2
108
     */
109
    public ?\Closure $isEmpty = null;
110
    /**
111
     * @var \Closure|array|string custom method to getEntityName
112
     * the callback function must return a string
113
     */
114
    public $getEntityName;
115
    /**
116
     * @var \Closure|array|string custom method to getEntityId
117
     * the callback function can return a string or array
118
     */
119
    public $getEntityId;
120
    /**
121
     * [
122
     *  'title' => [
123
     *      'new' => ['value' => 'New title'],
124
     *  ],
125
     *  'is_publish' => [
126
     *      'old' => ['value' => false],
127
     *      'new' => ['value' => true],
128
     *  ],
129
     *  'status' => [
130
     *      'old' => ['id' => 0, 'value' => 'Disabled'],
131
     *      'new' => ['id' => 1, 'value' => 'Active'],
132
     *  ],
133
     *  'owner_id' => [
134
     *      'old' => ['id' => 1, 'value' => 'admin'],
135
     *      'new' => ['id' => 2, 'value' => 'lucy'],
136
     *  ]
137
     * ]
138
     */
139
    private array $changedAttributes = [];
140
141
    private string $action;
142
143
    private ManagerInterface $logger;
144
145 26
    public function __construct(
146
        ManagerInterface $logger,
147
        array            $config = []
148
    )
149
    {
150 26
        parent::__construct($config);
151 26
        $this->logger = $logger;
152
    }
153
154 26
    public function init(): void
155
    {
156 26
        $this->initAttributes();
157
    }
158
159 26
    private function initAttributes(): void
160
    {
161 26
        foreach ($this->attributes as $key => $value) {
162 25
            if (is_int($key)) {
163 25
                unset($this->attributes[$key]);
164 25
                $this->attributes[$value] = [];
165
            }
166
        }
167
    }
168
169 26
    public function events(): array
170
    {
171 26
        if (false === $this->logger->isEnabled()) {
172 1
            return [];
173
        }
174 25
        return [
175 25
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
176 25
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
177 25
            ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
178 25
            ActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
179 25
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
180 25
        ];
181
    }
182
183 24
    public function beforeSave(): void
184
    {
185 24
        $this->changedAttributes = $this->prepareChangedAttributes();
186 24
        $this->action = $this->owner->getIsNewRecord() ? 'created' : 'updated';
187
    }
188
189 24
    public function afterSave(): void
190
    {
191 24
        if (empty($this->changedAttributes)) {
192 4
            return;
193
        }
194 24
        $this->saveMessage($this->action, $this->changedAttributes);
195
    }
196
197 4
    public function beforeDelete(): void
198
    {
199 4
        if (false === $this->softDelete) {
200 3
            $this->logger->delete(new DeleteCommand([
201 3
                'entityName' => $this->getEntityName(),
202 3
                'entityId' => $this->getEntityId(),
203 3
            ]));
204
        }
205 4
        $this->saveMessage('deleted', $this->prepareChangedAttributes(true));
206
    }
207
208 24
    private function prepareChangedAttributes(bool $unset = false): array
209
    {
210 24
        $result = [];
211 24
        foreach ($this->attributes as $attribute => $options) {
212 24
            $old = $this->owner->getOldAttribute($attribute);
213 24
            $new = false === $unset ? $this->owner->getAttribute($attribute) : null;
214
215 24
            if ($this->isEmpty($old) && $this->isEmpty($new)) {
216 24
                continue;
217
            }
218 24
            if (false === $unset && false === $this->owner->isAttributeChanged($attribute, $this->identicalAttributes)) {
219 12
                continue;
220
            }
221 24
            $result[$attribute] = $this->resolveStoreValues($old, $new, $options);
222
        }
223 24
        return $result;
224
    }
225
226 24
    private function filterStoreValues(array $data): array
227
    {
228 24
        if (isset($data['old']) && !isset($data['old']['value'])) {
229 24
            unset($data['old']);
230
        }
231 24
        return $data;
232
    }
233
234
    /**
235
     * @param string|int $old_id
236
     * @param string|int $new_id
237
     */
238 24
    protected function resolveStoreValues($old_id, $new_id, array $options): array
239
    {
240 24
        if (isset($options['list'])) {
241 24
            $value = $this->resolveListValues($old_id, $new_id, $options['list']);
242 22
        } elseif (isset($options['relation'], $options['attribute'])) {
243 17
            $value = $this->resolveRelationValues($old_id, $new_id, $options['relation'], $options['attribute']);
244
        } else {
245 22
            $value = $this->resolveSimpleValues($old_id, $new_id);
246
        }
247 24
        return $this->filterStoreValues($value);
248
    }
249
250
    /**
251
     * @param string|int $old_id
252
     * @param string|int $new_id
253
     */
254 22
    private function resolveSimpleValues($old_id, $new_id): array
255
    {
256 22
        if ($old_id instanceof ArrayExpression || $old_id instanceof JsonExpression) {
0 ignored issues
show
introduced by
$old_id is never a sub-type of yii\db\JsonExpression.
Loading history...
257
            $old_id = $old_id->getValue();
258
        }
259 22
        if ($new_id instanceof ArrayExpression || $new_id instanceof JsonExpression) {
0 ignored issues
show
introduced by
$new_id is never a sub-type of yii\db\JsonExpression.
Loading history...
260
            $new_id = $new_id->getValue();
261
        }
262 22
        return [
263 22
            'old' => ['value' => $old_id],
264 22
            'new' => ['value' => $new_id],
265 22
        ];
266
    }
267
268
    /**
269
     * @param string|int|array $old_id
270
     * @param string|int|array $new_id
271
     */
272 24
    private function resolveListValues($old_id, $new_id, string $listName): array
273
    {
274 24
        $old['id'] = $old_id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$old was never initialized. Although not strictly required by PHP, it is generally a good practice to add $old = array(); before regardless.
Loading history...
275 24
        $new['id'] = $new_id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$new was never initialized. Although not strictly required by PHP, it is generally a good practice to add $new = array(); before regardless.
Loading history...
276 24
        $list = [];
277
278 24
        if (is_array($old_id) || is_array($new_id)) {
279 1
            $list = ArrayHelper::getValue($this->owner, $listName);
280
        }
281 24
        if (is_array($old_id)) {
282 1
            $old['value'] = array_intersect_key($list, array_flip($old_id));
283
        } else {
284 24
            $old['value'] = ($old_id === null) ? null : ArrayHelper::getValue($this->owner, [$listName, $old_id]);
0 ignored issues
show
introduced by
The condition $old_id === null is always false.
Loading history...
285
        }
286 24
        if (is_array($new_id)) {
287 1
            $new['value'] = array_intersect_key($list, array_flip($new_id));
288
        } else {
289 24
            $new['value'] = ArrayHelper::getValue($this->owner, [$listName, $new_id]);
290
        }
291 24
        return [
292 24
            'old' => $old,
293 24
            'new' => $new
294 24
        ];
295
    }
296
297
    /**
298
     * @param string|int $old_id
299
     * @param string|int $new_id
300
     */
301 17
    private function resolveRelationValues($old_id, $new_id, string $relation, string $attribute): array
302
    {
303 17
        $old['id'] = $old_id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$old was never initialized. Although not strictly required by PHP, it is generally a good practice to add $old = array(); before regardless.
Loading history...
304 17
        $new['id'] = $new_id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$new was never initialized. Although not strictly required by PHP, it is generally a good practice to add $new = array(); before regardless.
Loading history...
305
306 17
        $relationQuery = clone $this->owner->getRelation($relation);
307 17
        $relationQuery->primaryModel = null;
1 ignored issue
show
Bug introduced by
Accessing primaryModel on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
308 17
        $idAttribute = array_keys($relationQuery->link)[0];
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
309 17
        $targetId = array_filter([$old_id, $new_id]);
310
311 17
        $relationModels = $relationQuery
312 17
            ->where([$idAttribute => $targetId])
313 17
            ->indexBy($idAttribute)
314 17
            ->limit(count($targetId))
315 17
            ->all();
316
317 17
        $old['value'] = ($old_id === null) ? null : ArrayHelper::getValue($relationModels, [$old_id, $attribute]);
0 ignored issues
show
introduced by
The condition $old_id === null is always false.
Loading history...
318 17
        $new['value'] = ArrayHelper::getValue($relationModels, [$new_id, $attribute]);
319
320 17
        return [
321 17
            'old' => $old,
322 17
            'new' => $new
323 17
        ];
324
    }
325
326 24
    protected function saveMessage(string $action, array $data): void
327
    {
328 24
        $data = $this->beforeSaveMessage($data);
329 24
        $this->addLog($data, $action);
330 24
        $this->afterSaveMessage();
331
    }
332
333
    /**
334
     * @param string|array $data
335
     * @since 1.7.0
336
     */
337 24
    public function addLog($data, string $action = null): bool
338
    {
339
        /** @var MessageData $message */
340 24
        $message = Yii::createObject([
341 24
            '__class' => MessageData::class,
342 24
            'entityName' => $this->getEntityName(),
343 24
            'entityId' => $this->getEntityId(),
344 24
            'createdAt' => time(),
345 24
            'action' => $action,
346 24
            'data' => $data,
347 24
        ]);
348 24
        return $this->logger->log($message);
349
    }
350
351
    /**
352
     * @since 1.5.3
353
     */
354 24
    public function beforeSaveMessage(array $data): array
355
    {
356 24
        if (null !== $this->beforeSaveMessage) {
357 1
            return call_user_func($this->beforeSaveMessage, $data);
358
        }
359 23
        $name = self::EVENT_BEFORE_SAVE_MESSAGE;
360 23
        if (method_exists($this->owner, $name)) {
361 1
            return $this->owner->$name($data);
362
        }
363 22
        $event = new MessageEvent();
364 22
        $event->logData = $data;
365 22
        $this->owner->trigger($name, $event);
366 22
        return $event->logData;
367
    }
368
369
    /**
370
     * @since 1.5.3
371
     */
372 24
    public function afterSaveMessage(): void
373
    {
374 24
        $name = self::EVENT_AFTER_SAVE_MESSAGE;
375 24
        if (method_exists($this->owner, $name)) {
376 1
            $this->owner->$name();
377
        } else {
378 23
            $this->owner->trigger($name);
379
        }
380
    }
381
382 25
    public function getEntityName(): string
383
    {
384 25
        if (is_string($this->getEntityName)) {
385 25
            return $this->getEntityName;
386
        }
387 1
        if (is_callable($this->getEntityName)) {
388 1
            return call_user_func($this->getEntityName);
389
        }
390 1
        $class = get_class($this->owner);
391 1
        $class = StringHelper::basename($class);
392 1
        $this->getEntityName = Inflector::camel2id($class, '_');
393 1
        return $this->getEntityName;
394
    }
395
396 26
    public function getEntityId(): string
397
    {
398 26
        if (null === $this->getEntityId) {
399 26
            $result = $this->owner->getPrimaryKey();
400 3
        } elseif (is_callable($this->getEntityId)) {
401 3
            $result = call_user_func($this->getEntityId);
402
        } else {
403
            $result = $this->getEntityId;
404
        }
405 26
        if ($this->isEmpty($result)) {
406 1
            throw new InvalidValueException('the property "entityId" can not be empty');
407
        }
408 25
        if (is_array($result)) {
409 1
            ksort($result);
410 1
            $result = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
411
        }
412 25
        return $result;
413
    }
414
415
    /**
416
     * Checks if the given value is empty.
417
     * A value is considered empty if it is null, an empty array, or an empty string.
418
     * Note that this method is different from PHP empty(). It will return false when the value is 0.
419
     * @param mixed $value the value to be checked
420
     * @return bool whether the value is empty
421
     * @since 1.5.2
422
     */
423 26
    public function isEmpty($value): bool
424
    {
425 26
        if (null !== $this->isEmpty) {
426 1
            return call_user_func($this->isEmpty, $value);
427
        }
428 25
        return null === $value || '' === $value || [] === $value;
429
    }
430
}