Passed
Push — master ( e800ea...3ba9d6 )
by Quetzy
03:28
created

Auditable   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 532
Duplicated Lines 0 %

Test Coverage

Coverage 97.08%

Importance

Changes 0
Metric Value
wmc 69
dl 0
loc 532
ccs 166
cts 171
cp 0.9708
rs 2.8301
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A disableAuditing() 0 3 1
B resolveAttributeGetter() 0 9 5
D resolveAuditExclusions() 0 33 9
A getAuditStrict() 0 3 1
A getUpdatedEventAttributes() 0 15 3
A getRetrievedEventAttributes() 0 8 1
A getRestoredEventAttributes() 0 4 1
A getAuditEvents() 0 7 1
A getCreatedEventAttributes() 0 13 3
A getAuditInclude() 0 3 1
A getAuditEvent() 0 3 1
A getAuditTimestamps() 0 3 1
A isEventAuditable() 0 3 1
A bootAuditable() 0 4 2
A transformAudit() 0 3 1
A resolveUserAgent() 0 9 2
A readyForAuditing() 0 7 2
B toAudit() 0 35 4
A getAuditThreshold() 0 3 1
A generateTags() 0 3 1
A resolveIpAddress() 0 9 2
A resolveUser() 0 9 2
A enableAuditing() 0 3 1
C transitionTo() 0 42 7
A getAuditDriver() 0 3 1
A resolveUrl() 0 9 2
A audits() 0 5 1
A getAuditExclude() 0 3 1
A isAuditingEnabled() 0 7 2
A setAuditEvent() 0 5 2
A isAttributeAuditable() 0 12 3
A getDeletedEventAttributes() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like Auditable 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 Auditable, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Laravel Auditing package.
4
 *
5
 * @author     Antério Vieira <[email protected]>
6
 * @author     Quetzy Garcia  <[email protected]>
7
 * @author     Raphael França <[email protected]>
8
 * @copyright  2015-2018
9
 *
10
 * For the full copyright and license information,
11
 * please view the LICENSE.md file that was distributed
12
 * with this source code.
13
 */
14
15
namespace OwenIt\Auditing;
16
17
use Illuminate\Database\Eloquent\Relations\MorphMany;
18
use Illuminate\Support\Facades\App;
19
use Illuminate\Support\Facades\Config;
20
use OwenIt\Auditing\Contracts\IpAddressResolver;
21
use OwenIt\Auditing\Contracts\UrlResolver;
22
use OwenIt\Auditing\Contracts\UserAgentResolver;
23
use OwenIt\Auditing\Contracts\UserResolver;
24
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
25
use OwenIt\Auditing\Exceptions\AuditingException;
26
27
trait Auditable
28
{
29
    /**
30
     * Auditable attributes excluded from the Audit.
31
     *
32
     * @var array
33
     */
34
    protected $excludedAttributes = [];
35
36
    /**
37
     * Audit event name.
38
     *
39
     * @var string
40
     */
41
    protected $auditEvent;
42
43
    /**
44
     * Is auditing disabled?
45
     *
46
     * @var bool
47
     */
48
    public static $auditingDisabled = false;
49
50
    /**
51
     * Auditable boot logic.
52
     *
53
     * @return void
54
     */
55 237
    public static function bootAuditable()
56
    {
57 237
        if (static::isAuditingEnabled()) {
58 234
            static::observe(new AuditableObserver());
59
        }
60 237
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65 36
    public function audits(): MorphMany
66
    {
67 36
        return $this->morphMany(
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
68 36
            Config::get('audit.implementation', Models\Audit::class),
69 36
            'auditable'
70
        );
71
    }
72
73
    /**
74
     * Resolve the Auditable attributes to exclude from the Audit.
75
     *
76
     * @return void
77
     */
78 129
    protected function resolveAuditExclusions()
79
    {
80 129
        $this->excludedAttributes = $this->getAuditExclude();
81
82
        // When in strict mode, hidden and non visible attributes are excluded
83 129
        if ($this->getAuditStrict()) {
84
            // Hidden attributes
85
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
86
87
            // Non visible attributes
88
            if ($this->visible) {
89
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
90
91
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
92
            }
93
        }
94
95
        // Exclude Timestamps
96 129
        if (!$this->getAuditTimestamps()) {
97 129
            array_push($this->excludedAttributes, static::CREATED_AT, static::UPDATED_AT);
0 ignored issues
show
Bug introduced by
The constant OwenIt\Auditing\Auditable::CREATED_AT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant OwenIt\Auditing\Auditable::UPDATED_AT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
98
99 129
            if (defined('static::DELETED_AT')) {
100
                $this->excludedAttributes[] = static::DELETED_AT;
0 ignored issues
show
Bug introduced by
The constant OwenIt\Auditing\Auditable::DELETED_AT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
101
            }
102
        }
103
104
        // Valid attributes are all those that made it out of the exclusion array
105 129
        $attributes = array_except($this->attributes, $this->excludedAttributes);
106
107 129
        foreach ($attributes as $attribute => $value) {
108
            // Apart from null, non scalar values will be excluded
109 117
            if (is_object($value) && !method_exists($value, '__toString') || is_array($value)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: {currentAssign}, Probably Intended Meaning: {alternativeAssign}
Loading history...
110 117
                $this->excludedAttributes[] = $attribute;
111
            }
112
        }
113 129
    }
114
115
    /**
116
     * Get the old/new attributes of a retrieved event.
117
     *
118
     * @return array
119
     */
120 6
    protected function getRetrievedEventAttributes(): array
121
    {
122
        // This is a read event with no attribute changes,
123
        // only metadata will be stored in the Audit
124
125
        return [
126 6
            [],
127
            [],
128
        ];
129
    }
130
131
    /**
132
     * Get the old/new attributes of a created event.
133
     *
134
     * @return array
135
     */
136 111
    protected function getCreatedEventAttributes(): array
137
    {
138 111
        $new = [];
139
140 111
        foreach ($this->attributes as $attribute => $value) {
141 99
            if ($this->isAttributeAuditable($attribute)) {
142 99
                $new[$attribute] = $value;
143
            }
144
        }
145
146
        return [
147 111
            [],
148 111
            $new,
149
        ];
150
    }
151
152
    /**
153
     * Get the old/new attributes of an updated event.
154
     *
155
     * @return array
156
     */
157 12
    protected function getUpdatedEventAttributes(): array
158
    {
159 12
        $old = [];
160 12
        $new = [];
161
162 12
        foreach ($this->getDirty() as $attribute => $value) {
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

162
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
163 9
            if ($this->isAttributeAuditable($attribute)) {
164 9
                $old[$attribute] = array_get($this->original, $attribute);
165 9
                $new[$attribute] = array_get($this->attributes, $attribute);
166
            }
167
        }
168
169
        return [
170 12
            $old,
171 12
            $new,
172
        ];
173
    }
174
175
    /**
176
     * Get the old/new attributes of a deleted event.
177
     *
178
     * @return array
179
     */
180 12
    protected function getDeletedEventAttributes(): array
181
    {
182 12
        $old = [];
183
184 12
        foreach ($this->attributes as $attribute => $value) {
185 12
            if ($this->isAttributeAuditable($attribute)) {
186 12
                $old[$attribute] = $value;
187
            }
188
        }
189
190
        return [
191 12
            $old,
192
            [],
193
        ];
194
    }
195
196
    /**
197
     * Get the old/new attributes of a restored event.
198
     *
199
     * @return array
200
     */
201 6
    protected function getRestoredEventAttributes(): array
202
    {
203
        // A restored event is just a deleted event in reverse
204 6
        return array_reverse($this->getDeletedEventAttributes());
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 162
    public function readyForAuditing(): bool
211
    {
212 162
        if (static::$auditingDisabled) {
213 3
            return false;
214
        }
215
216 162
        return $this->isEventAuditable($this->auditEvent);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 144
    public function toAudit(): array
223
    {
224 144
        if (!$this->readyForAuditing()) {
225 3
            throw new AuditingException('A valid audit event has not been set');
226
        }
227
228 141
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
229
230 141
        if (!method_exists($this, $attributeGetter)) {
231 12
            throw new AuditingException(sprintf(
232 12
                'Unable to handle "%s" event, %s() method missing',
233 12
                $this->auditEvent,
234 12
                $attributeGetter
235
            ));
236
        }
237
238 129
        $this->resolveAuditExclusions();
239
240 129
        list($old, $new) = call_user_func([$this, $attributeGetter]);
241
242 129
        $userForeignKey = Config::get('audit.user.foreign_key', 'user_id');
243
244 129
        $tags = implode(',', $this->generateTags());
245
246 129
        return $this->transformAudit([
247 129
            'old_values'     => $old,
248 129
            'new_values'     => $new,
249 129
            'event'          => $this->auditEvent,
250 129
            'auditable_id'   => $this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

250
            'auditable_id'   => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
251 129
            'auditable_type' => $this->getMorphClass(),
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

251
            'auditable_type' => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
252 129
            $userForeignKey  => $this->resolveUser(),
253 126
            'url'            => $this->resolveUrl(),
254 123
            'ip_address'     => $this->resolveIpAddress(),
255 120
            'user_agent'     => $this->resolveUserAgent(),
256 117
            'tags'           => empty($tags) ? null : $tags,
257
        ]);
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263 114
    public function transformAudit(array $data): array
264
    {
265 114
        return $data;
266
    }
267
268
    /**
269
     * Resolve the User.
270
     *
271
     * @throws AuditingException
272
     *
273
     * @return mixed|null
274
     */
275 129
    protected function resolveUser()
276
    {
277 129
        $userResolver = Config::get('audit.resolver.user');
278
279 129
        if (is_subclass_of($userResolver, UserResolver::class)) {
280 126
            return call_user_func([$userResolver, 'resolve']);
281
        }
282
283 3
        throw new AuditingException('Invalid UserResolver implementation');
284
    }
285
286
    /**
287
     * Resolve the URL.
288
     *
289
     * @throws AuditingException
290
     *
291
     * @return string
292
     */
293 126
    protected function resolveUrl(): string
294
    {
295 126
        $urlResolver = Config::get('audit.resolver.url');
296
297 126
        if (is_subclass_of($urlResolver, UrlResolver::class)) {
298 123
            return call_user_func([$urlResolver, 'resolve']);
299
        }
300
301 3
        throw new AuditingException('Invalid UrlResolver implementation');
302
    }
303
304
    /**
305
     * Resolve the IP Address.
306
     *
307
     * @throws AuditingException
308
     *
309
     * @return string
310
     */
311 123
    protected function resolveIpAddress(): string
312
    {
313 123
        $ipAddressResolver = Config::get('audit.resolver.ip_address');
314
315 123
        if (is_subclass_of($ipAddressResolver, IpAddressResolver::class)) {
316 120
            return call_user_func([$ipAddressResolver, 'resolve']);
317
        }
318
319 3
        throw new AuditingException('Invalid IpAddressResolver implementation');
320
    }
321
322
    /**
323
     * Resolve the User Agent.
324
     *
325
     * @throws AuditingException
326
     *
327
     * @return string|null
328
     */
329 120
    protected function resolveUserAgent()
330
    {
331 120
        $userAgentResolver = Config::get('audit.resolver.user_agent');
332
333 120
        if (is_subclass_of($userAgentResolver, UserAgentResolver::class)) {
334 117
            return call_user_func([$userAgentResolver, 'resolve']);
335
        }
336
337 3
        throw new AuditingException('Invalid UserAgentResolver implementation');
338
    }
339
340
    /**
341
     * Determine if an attribute is eligible for auditing.
342
     *
343
     * @param string $attribute
344
     *
345
     * @return bool
346
     */
347 114
    protected function isAttributeAuditable(string $attribute): bool
348
    {
349
        // The attribute should not be audited
350 114
        if (in_array($attribute, $this->excludedAttributes)) {
351 105
            return false;
352
        }
353
354
        // The attribute is auditable when explicitly
355
        // listed or when the include array is empty
356 114
        $include = $this->getAuditInclude();
357
358 114
        return in_array($attribute, $include) || empty($include);
359
    }
360
361
    /**
362
     * Determine whether an event is auditable.
363
     *
364
     * @param string $event
365
     *
366
     * @return bool
367
     */
368 165
    protected function isEventAuditable($event): bool
369
    {
370 165
        return is_string($this->resolveAttributeGetter($event));
371
    }
372
373
    /**
374
     * Attribute getter method resolver.
375
     *
376
     * @param string $event
377
     *
378
     * @return string|null
379
     */
380 165
    protected function resolveAttributeGetter($event)
381
    {
382 165
        foreach ($this->getAuditEvents() as $key => $value) {
383 165
            $auditableEvent = is_int($key) ? $value : $key;
384
385 165
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
386
387 165
            if (preg_match($auditableEventRegex, $event)) {
388 165
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
389
            }
390
        }
391 63
    }
392
393
    /**
394
     * {@inheritdoc}
395
     */
396 165
    public function setAuditEvent(string $event): Contracts\Auditable
397
    {
398 165
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
399
400 165
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     */
406 6
    public function getAuditEvent()
407
    {
408 6
        return $this->auditEvent;
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     */
414 174
    public function getAuditEvents(): array
415
    {
416 174
        return $this->auditEvents ?? Config::get('audit.events', [
0 ignored issues
show
Bug introduced by
The property auditEvents does not exist on OwenIt\Auditing\Auditable. Did you mean auditEvent?
Loading history...
417 174
            'created',
418
            'updated',
419
            'deleted',
420
            'restored',
421
        ]);
422
    }
423
424
    /**
425
     * Disable Auditing.
426
     *
427
     * @return void
428
     */
429 3
    public static function disableAuditing()
430
    {
431 3
        static::$auditingDisabled = true;
432 3
    }
433
434
    /**
435
     * Enable Auditing.
436
     *
437
     * @return void
438
     */
439 3
    public static function enableAuditing()
440
    {
441 3
        static::$auditingDisabled = false;
442 3
    }
443
444
    /**
445
     * Determine whether auditing is enabled.
446
     *
447
     * @return bool
448
     */
449 246
    public static function isAuditingEnabled(): bool
450
    {
451 246
        if (App::runningInConsole()) {
452 240
            return Config::get('audit.console', false);
453
        }
454
455 6
        return true;
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461 120
    public function getAuditInclude(): array
462
    {
463 120
        return $this->auditInclude ?? [];
464
    }
465
466
    /**
467
     * {@inheritdoc}
468
     */
469 135
    public function getAuditExclude(): array
470
    {
471 135
        return $this->auditExclude ?? [];
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477 138
    public function getAuditStrict(): bool
478
    {
479 138
        return $this->auditStrict ?? Config::get('audit.strict', false);
480
    }
481
482
    /**
483
     * {@inheritdoc}
484
     */
485 138
    public function getAuditTimestamps(): bool
486
    {
487 138
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
488
    }
489
490
    /**
491
     * {@inheritdoc}
492
     */
493 129
    public function getAuditDriver()
494
    {
495 129
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
496
    }
497
498
    /**
499
     * {@inheritdoc}
500
     */
501 120
    public function getAuditThreshold(): int
502
    {
503 120
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     */
509 132
    public function generateTags(): array
510
    {
511 132
        return [];
512
    }
513
514
    /**
515
     * {@inheritdoc}
516
     */
517 24
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
518
    {
519
        // The Audit must be for an Auditable model of this type
520 24
        if ($this->getMorphClass() !== $audit->auditable_type) {
0 ignored issues
show
Bug introduced by
Accessing auditable_type on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
521 6
            throw new AuditableTransitionException(sprintf(
522 6
                'Expected Auditable type %s, got %s instead',
523 6
                $this->getMorphClass(),
524 6
                $audit->auditable_type
525
            ));
526
        }
527
528
        // The Audit must be for this specific Auditable model
529 18
        if ($this->getKey() !== $audit->auditable_id) {
0 ignored issues
show
Bug introduced by
Accessing auditable_id on the interface OwenIt\Auditing\Contracts\Audit suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
530 3
            throw new AuditableTransitionException(sprintf(
531 3
                'Expected Auditable id %s, got %s instead',
532 3
                $this->getKey(),
533 3
                $audit->auditable_id
534
            ));
535
        }
536
537
        // The attribute compatibility between the Audit and the Auditable model must be met
538 15
        $modified = $audit->getModified();
539
540 15
        if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
0 ignored issues
show
Bug introduced by
It seems like $modified can also be of type string; however, parameter $array1 of array_diff_key() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

540
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
Bug introduced by
It seems like getAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

540
        if ($incompatibilities = array_diff_key($modified, $this->/** @scrutinizer ignore-call */ getAttributes())) {
Loading history...
541 3
            throw new AuditableTransitionException(sprintf(
542 3
                'Incompatibility between [%s:%s] and [%s:%s]',
543 3
                $this->getMorphClass(),
544 3
                $this->getKey(),
545 3
                get_class($audit),
546 3
                $audit->getKey()
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on OwenIt\Auditing\Contracts\Audit. Since it exists in all sub-types, consider adding an abstract or default implementation to OwenIt\Auditing\Contracts\Audit. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

546
                $audit->/** @scrutinizer ignore-call */ 
547
                        getKey()
Loading history...
547 3
            ), array_keys($incompatibilities));
548
        }
549
550 12
        $key = $old ? 'old' : 'new';
551
552 12
        foreach ($modified as $attribute => $value) {
553 9
            if (array_key_exists($key, $value)) {
554 9
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

554
                $this->/** @scrutinizer ignore-call */ 
555
                       setAttribute($attribute, $value[$key]);
Loading history...
555
            }
556
        }
557
558 12
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
559
    }
560
}
561