Test Failed
Pull Request — master (#684)
by Morten
05:37
created

Auditable::toAudit()   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.332

Importance

Changes 10
Bugs 2 Features 2
Metric Value
cc 10
eloc 28
c 10
b 2
f 2
nc 4
nop 0
dl 0
loc 46
ccs 20
cts 28
cp 0.7143
crap 12.332
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Relations\MorphMany;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Facades\App;
9
use Illuminate\Support\Facades\Config;
10
use OwenIt\Auditing\Contracts\AttributeEncoder;
11
use OwenIt\Auditing\Contracts\AttributeRedactor;
12
use OwenIt\Auditing\Contracts\Resolver;
13
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
14
use OwenIt\Auditing\Exceptions\AuditingException;
15
16
trait Auditable
17
{
18
    /**
19
     * Auditable attributes excluded from the Audit.
20
     *
21
     * @var array
22
     */
23
    protected $excludedAttributes = [];
24
25
    /**
26
     * Audit event name.
27
     *
28
     * @var string
29
     */
30
    protected $auditEvent;
31
32
    /**
33
     * Is auditing disabled?
34
     *
35
     * @var bool
36
     */
37
    public static $auditingDisabled = false;
38
39
    /**
40
     * @var array
41
     */
42
    protected $auditExclude;
43
44
    /**
45
     * Property may set custom event data to register
46
     * @var null|array
47
     */
48
    public $auditCustomOld = null;
49
50
    /**
51
     * Property may set custom event data to register
52
     * @var null|array
53
     */
54
    public $auditCustomNew = null;
55
56
    /**
57
     * If this is a custom event (as opposed to an eloquent event
58
     * @var bool
59
     */
60
    public $isCustomEvent = false;
61
62
    /**
63
     * Auditable boot logic.
64
     *
65
     * @return void
66
     */
67 14
    public static function bootAuditable()
68
    {
69 14
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
70 12
            static::observe(new AuditableObserver());
71
        }
72 14
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function audits(): MorphMany
78
    {
79
        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

79
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
80
            Config::get('audit.implementation', Models\Audit::class),
81
            'auditable'
82
        );
83
    }
84
85
    /**
86
     * Resolve the Auditable attributes to exclude from the Audit.
87
     *
88
     * @return void
89
     */
90 12
    protected function resolveAuditExclusions()
91
    {
92 12
        $this->excludedAttributes = $this->getAuditExclude();
93
94
        // When in strict mode, hidden and non visible attributes are excluded
95 12
        if ($this->getAuditStrict()) {
96
            // Hidden attributes
97
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
98
99
            // Non visible attributes
100
            if ($this->visible) {
101
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
102
103
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
104
            }
105
        }
106
107
        // Exclude Timestamps
108 12
        if (!$this->getAuditTimestamps()) {
109 12
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->getUpdatedAtColumn());
0 ignored issues
show
Bug introduced by
It seems like getUpdatedAtColumn() 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

109
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
Bug introduced by
It seems like getCreatedAtColumn() 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

109
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
110
111 12
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
112 4
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
0 ignored issues
show
Bug introduced by
It seems like getDeletedAtColumn() 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

112
                /** @scrutinizer ignore-call */ 
113
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
113
            }
114
        }
115
116
        // Valid attributes are all those that made it out of the exclusion array
117 12
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
118
119 12
        foreach ($attributes as $attribute => $value) {
120
            // Apart from null, non scalar values will be excluded
121 12
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
122
                $this->excludedAttributes[] = $attribute;
123
            }
124
        }
125 12
    }
126
127
    /**
128
     * Get the old/new attributes of a retrieved event.
129
     *
130
     * @return array
131
     */
132 4
    protected function getRetrievedEventAttributes(): array
133
    {
134
        // This is a read event with no attribute changes,
135
        // only metadata will be stored in the Audit
136
137
        return [
138 4
            [],
139
            [],
140
        ];
141
    }
142
143
    /**
144
     * Get the old/new attributes of a created event.
145
     *
146
     * @return array
147
     */
148 10
    protected function getCreatedEventAttributes(): array
149
    {
150 10
        $new = [];
151
152 10
        foreach ($this->attributes as $attribute => $value) {
153 10
            if ($this->isAttributeAuditable($attribute)) {
154 10
                $new[$attribute] = $value;
155
            }
156
        }
157
158
        return [
159 10
            [],
160 10
            $new,
161
        ];
162
    }
163
164
    protected function getCustomEventAttributes(): array
165
    {
166
        return [
167
            $this->auditCustomOld,
168
            $this->auditCustomNew
169
        ];
170
    }
171
172
    /**
173
     * Get the old/new attributes of an updated event.
174
     *
175
     * @return array
176
     */
177
    protected function getUpdatedEventAttributes(): array
178
    {
179
        $old = [];
180
        $new = [];
181
182
        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

182
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
183
            if ($this->isAttributeAuditable($attribute)) {
184
                $old[$attribute] = Arr::get($this->original, $attribute);
185
                $new[$attribute] = Arr::get($this->attributes, $attribute);
186
            }
187
        }
188
189
        return [
190
            $old,
191
            $new,
192
        ];
193
    }
194
195
    /**
196
     * Get the old/new attributes of a deleted event.
197
     *
198
     * @return array
199
     */
200
    protected function getDeletedEventAttributes(): array
201
    {
202
        $old = [];
203
204
        foreach ($this->attributes as $attribute => $value) {
205
            if ($this->isAttributeAuditable($attribute)) {
206
                $old[$attribute] = $value;
207
            }
208
        }
209
210
        return [
211
            $old,
212
            [],
213
        ];
214
    }
215
216
    /**
217
     * Get the old/new attributes of a restored event.
218
     *
219
     * @return array
220
     */
221
    protected function getRestoredEventAttributes(): array
222
    {
223
        // A restored event is just a deleted event in reverse
224
        return array_reverse($this->getDeletedEventAttributes());
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 12
    public function readyForAuditing(): bool
231
    {
232 12
        if (static::$auditingDisabled) {
233
            return false;
234
        }
235
236 12
        if ($this->isCustomEvent) {
237
            return true;
238
        }
239
240 12
        return $this->isEventAuditable($this->auditEvent);
241
    }
242
243
    /**
244
     * Modify attribute value.
245
     *
246
     * @param string $attribute
247
     * @param mixed $value
248
     *
249
     * @return mixed
250
     * @throws AuditingException
251
     *
252
     */
253
    protected function modifyAttributeValue(string $attribute, $value)
254
    {
255
        $attributeModifiers = $this->getAttributeModifiers();
256
257
        if (!array_key_exists($attribute, $attributeModifiers)) {
258
            return $value;
259
        }
260
261
        $attributeModifier = $attributeModifiers[$attribute];
262
263
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
264
            return call_user_func([$attributeModifier, 'redact'], $value);
265
        }
266
267
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
268
            return call_user_func([$attributeModifier, 'encode'], $value);
269
        }
270
271
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 12
    public function toAudit(): array
278
    {
279 12
        if (!$this->readyForAuditing()) {
280
            throw new AuditingException('A valid audit event has not been set');
281
        }
282
283 12
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
284
285 12
        if (!method_exists($this, $attributeGetter)) {
0 ignored issues
show
Bug introduced by
It seems like $attributeGetter can also be of type null; however, parameter $method of method_exists() does only seem to accept string, 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

285
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
286
            throw new AuditingException(sprintf(
287
                'Unable to handle "%s" event, %s() method missing',
288
                $this->auditEvent,
289
                $attributeGetter
290
            ));
291
        }
292
293 12
        $this->resolveAuditExclusions();
294
295 12
        list($old, $new) = $this->$attributeGetter();
296
297 12
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
298
            foreach ($old as $attribute => $value) {
299
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
300
            }
301
302
            foreach ($new as $attribute => $value) {
303
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
304
            }
305
        }
306
307 12
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
308
309 12
        $tags = implode(',', $this->generateTags());
310
311 12
        $user = $this->resolveUser();
312
313 12
        return $this->transformAudit(array_merge([
314 12
            'old_values'           => $old,
315 12
            'new_values'           => $new,
316 12
            'event'                => $this->auditEvent,
317 12
            '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

317
            'auditable_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
318 12
            '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

318
            'auditable_type'       => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
319 12
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
320 12
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
321 12
            'tags'                 => empty($tags) ? null : $tags,
322 12
        ], $this->runResolvers()));
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328 12
    public function transformAudit(array $data): array
329
    {
330 12
        return $data;
331
    }
332
333
    /**
334
     * Resolve the User.
335
     *
336
     * @return mixed|null
337
     * @throws AuditingException
338
     *
339
     */
340 12
    protected function resolveUser()
341
    {
342 12
        $userResolver = Config::get('audit.user.resolver');
343
344 12
        if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
345 12
            return call_user_func([$userResolver, 'resolve']);
346
        }
347
348
        throw new AuditingException('Invalid UserResolver implementation');
349
    }
350
351 12
    protected function runResolvers(): array
352
    {
353 12
        $resolved = [];
354 12
        foreach (Config::get('audit.resolvers', []) as $name => $implementation) {
355 12
            if (empty($implementation)) {
356
                continue;
357
            }
358
359 12
            if (!is_subclass_of($implementation, Resolver::class)) {
360
                throw new AuditingException('Invalid Resolver implementation for: ' . $name);
361
            }
362 12
            $resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
363
        }
364 12
        return $resolved;
365
    }
366
367
    /**
368
     * Determine if an attribute is eligible for auditing.
369
     *
370
     * @param string $attribute
371
     *
372
     * @return bool
373
     */
374 10
    protected function isAttributeAuditable(string $attribute): bool
375
    {
376
        // The attribute should not be audited
377 10
        if (in_array($attribute, $this->excludedAttributes, true)) {
378 10
            return false;
379
        }
380
381
        // The attribute is auditable when explicitly
382
        // listed or when the include array is empty
383 10
        $include = $this->getAuditInclude();
384
385 10
        return empty($include) || in_array($attribute, $include, true);
386
    }
387
388
    /**
389
     * Determine whether an event is auditable.
390
     *
391
     * @param string $event
392
     *
393
     * @return bool
394
     */
395 12
    protected function isEventAuditable($event): bool
396
    {
397 12
        return is_string($this->resolveAttributeGetter($event));
398
    }
399
400
    /**
401
     * Attribute getter method resolver.
402
     *
403
     * @param string $event
404
     *
405
     * @return string|null
406
     */
407 12
    protected function resolveAttributeGetter($event)
408
    {
409 12
        if (empty($event)) {
410 4
            return;
411
        }
412
413 12
        if ($this->isCustomEvent) {
414
            return 'getCustomEventAttributes';
415
        }
416
417 12
        foreach ($this->getAuditEvents() as $key => $value) {
418 12
            $auditableEvent = is_int($key) ? $value : $key;
419
420 12
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
421
422 12
            if (preg_match($auditableEventRegex, $event)) {
423 12
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
424
            }
425
        }
426 4
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 12
    public function setAuditEvent(string $event): Contracts\Auditable
432
    {
433 12
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
434
435 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...
436
    }
437
438
    public function setAuditExcludedAttributes(array $excludedAttributes)
439
    {
440
        $this->auditExclude = $excludedAttributes;
441
    }
442
443
    /**
444
     * {@inheritdoc}
445
     */
446
    public function getAuditEvent()
447
    {
448
        return $this->auditEvent;
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454 12
    public function getAuditEvents(): array
455
    {
456 12
        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...
457 12
                'created',
458
                'updated',
459
                'deleted',
460
                'restored',
461
            ]);
462
    }
463
464
    /**
465
     * Disable Auditing.
466
     *
467
     * @return void
468
     */
469
    public static function disableAuditing()
470
    {
471
        static::$auditingDisabled = true;
472
    }
473
474
    /**
475
     * Enable Auditing.
476
     *
477
     * @return void
478
     */
479
    public static function enableAuditing()
480
    {
481
        static::$auditingDisabled = false;
482
    }
483
484
    /**
485
     * Determine whether auditing is enabled.
486
     *
487
     * @return bool
488
     */
489 14
    public static function isAuditingEnabled(): bool
490
    {
491 14
        if (App::runningInConsole()) {
492 12
            return Config::get('audit.enabled', true) && Config::get('audit.console', false);
493
        }
494
495 2
        return Config::get('audit.enabled', true);
496
    }
497
498
    /**
499
     * {@inheritdoc}
500
     */
501 10
    public function getAuditInclude(): array
502
    {
503 10
        return $this->auditInclude ?? [];
0 ignored issues
show
Bug introduced by
The property auditInclude does not exist on OwenIt\Auditing\Auditable. Did you mean auditExclude?
Loading history...
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     */
509 12
    public function getAuditExclude(): array
510
    {
511 12
        return $this->auditExclude ?? [];
512
    }
513
514
    /**
515
     * {@inheritdoc}
516
     */
517 12
    public function getAuditStrict(): bool
518
    {
519 12
        return $this->auditStrict ?? Config::get('audit.strict', false);
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 12
    public function getAuditTimestamps(): bool
526
    {
527 12
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533 12
    public function getAuditDriver()
534
    {
535 12
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 12
    public function getAuditThreshold(): int
542
    {
543 12
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
544
    }
545
546
    /**
547
     * {@inheritdoc}
548
     */
549 12
    public function getAttributeModifiers(): array
550
    {
551 12
        return $this->attributeModifiers ?? [];
552
    }
553
554
    /**
555
     * {@inheritdoc}
556
     */
557 12
    public function generateTags(): array
558
    {
559 12
        return [];
560
    }
561
562
    /**
563
     * {@inheritdoc}
564
     */
565
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
566
    {
567
        // The Audit must be for an Auditable model of this type
568
        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...
569
            throw new AuditableTransitionException(sprintf(
570
                'Expected Auditable type %s, got %s instead',
571
                $this->getMorphClass(),
572
                $audit->auditable_type
573
            ));
574
        }
575
576
        // The Audit must be for this specific Auditable model
577
        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...
578
            throw new AuditableTransitionException(sprintf(
579
                'Expected Auditable id %s, got %s instead',
580
                $this->getKey(),
581
                $audit->auditable_id
582
            ));
583
        }
584
585
        // Redacted data should not be used when transitioning states
586
        foreach ($this->getAttributeModifiers() as $attribute => $modifier) {
587
            if (is_subclass_of($modifier, AttributeRedactor::class)) {
588
                throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set');
589
            }
590
        }
591
592
        // The attribute compatibility between the Audit and the Auditable model must be met
593
        $modified = $audit->getModified();
594
595
        if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on OwenIt\Auditing\Auditable. Did you maybe mean getAttributeModifiers()? ( Ignorable by Annotation )

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

595
        if ($incompatibilities = array_diff_key($modified, $this->/** @scrutinizer ignore-call */ getAttributes())) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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

595
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
596
            throw new AuditableTransitionException(sprintf(
597
                'Incompatibility between [%s:%s] and [%s:%s]',
598
                $this->getMorphClass(),
599
                $this->getKey(),
600
                get_class($audit),
601
                $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

601
                $audit->/** @scrutinizer ignore-call */ 
602
                        getKey()
Loading history...
602
            ), array_keys($incompatibilities));
603
        }
604
605
        $key = $old ? 'old' : 'new';
606
607
        foreach ($modified as $attribute => $value) {
608
            if (array_key_exists($key, $value)) {
609
                $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

609
                $this->/** @scrutinizer ignore-call */ 
610
                       setAttribute($attribute, $value[$key]);
Loading history...
610
            }
611
        }
612
613
        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...
614
    }
615
}
616