Passed
Push — master ( 5a5020...8fcdae )
by Loban
02:52
created

ActiveLogBehavior   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Test Coverage

Coverage 96.93%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 59
eloc 158
c 8
b 0
f 0
dl 0
loc 377
ccs 158
cts 163
cp 0.9693
rs 4.08

19 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 saveMessage() 0 5 1
B resolveListValues() 0 27 7
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 37 5
A afterSave() 0 6 2

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