Test Failed
Pull Request — master (#674)
by Orkhan
05:19
created

Auditable::getAttributeModifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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\IpAddressResolver;
13
use OwenIt\Auditing\Contracts\UrlResolver;
14
use OwenIt\Auditing\Contracts\UserAgentResolver;
15
use OwenIt\Auditing\Contracts\UserResolver;
16
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
17
use OwenIt\Auditing\Exceptions\AuditingException;
18
19
trait Auditable
20
{
21
    /**
22
     * Auditable attributes excluded from the Audit.
23
     *
24
     * @var array
25
     */
26
    protected $excludedAttributes = [];
27
28
    /**
29
     * Audit event name.
30
     *
31
     * @var string
32
     */
33
    protected $auditEvent;
34
35
    /**
36
     * Is auditing disabled?
37
     *
38
     * @var bool
39
     */
40
    public static $auditingDisabled = false;
41
42
    /**
43
     * Auditable boot logic.
44
     *
45
     * @return void
46
     */
47 14
    public static function bootAuditable()
48
    {
49 14
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
50 12
            static::observe(new AuditableObserver());
51
        }
52 14
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function audits(): MorphMany
58
    {
59
        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

59
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
60
            Config::get('audit.implementation', Models\Audit::class),
61
            'auditable'
62
        );
63
    }
64
65
    /**
66
     * Resolve the Auditable attributes to exclude from the Audit.
67
     *
68
     * @return void
69
     */
70 12
    protected function resolveAuditExclusions()
71
    {
72 12
        $this->excludedAttributes = $this->getAuditExclude();
73
74
        // When in strict mode, hidden and non visible attributes are excluded
75 12
        if ($this->getAuditStrict()) {
76
            // Hidden attributes
77
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
78
79
            // Non visible attributes
80
            if ($this->visible) {
81
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
82
83
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
84
            }
85
        }
86
87
        // Exclude Timestamps
88 12
        if (!$this->getAuditTimestamps()) {
89 12
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->getUpdatedAtColumn());
0 ignored issues
show
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

89
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
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

89
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
90
91 12
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
92 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

92
                /** @scrutinizer ignore-call */ 
93
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
93
            }
94
        }
95
96
        // Valid attributes are all those that made it out of the exclusion array
97 12
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
98
99 12
        foreach ($attributes as $attribute => $value) {
100
            // Apart from null, non scalar values will be excluded
101 12
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
102
                $this->excludedAttributes[] = $attribute;
103
            }
104
        }
105 12
    }
106
107
    /**
108
     * Get the old/new attributes of a retrieved event.
109
     *
110
     * @return array
111
     */
112 4
    protected function getRetrievedEventAttributes(): array
113
    {
114
        // This is a read event with no attribute changes,
115
        // only metadata will be stored in the Audit
116
117
        return [
118 4
            [],
119
            [],
120
        ];
121
    }
122
123
    /**
124
     * Get the old/new attributes of a created event.
125
     *
126
     * @return array
127
     */
128 10
    protected function getCreatedEventAttributes(): array
129
    {
130 10
        $new = [];
131
132 10
        foreach ($this->attributes as $attribute => $value) {
133 10
            if ($this->isAttributeAuditable($attribute)) {
134 10
                $new[$attribute] = $value;
135
            }
136
        }
137
138
        return [
139 10
            [],
140 10
            $new,
141
        ];
142
    }
143
144
    /**
145
     * Get the old/new attributes of an updated event.
146
     *
147
     * @return array
148
     */
149
    protected function getUpdatedEventAttributes(): array
150
    {
151
        $old = [];
152
        $new = [];
153
154
        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

154
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
155
            if ($this->isAttributeAuditable($attribute)) {
156
                $old[$attribute] = Arr::get($this->original, $attribute);
157
                $new[$attribute] = Arr::get($this->attributes, $attribute);
158
            }
159
        }
160
161
        return [
162
            $old,
163
            $new,
164
        ];
165
    }
166
167
    /**
168
     * Get the old/new attributes of a deleted event.
169
     *
170
     * @return array
171
     */
172
    protected function getDeletedEventAttributes(): array
173
    {
174
        $old = [];
175
176
        foreach ($this->attributes as $attribute => $value) {
177
            if ($this->isAttributeAuditable($attribute)) {
178
                $old[$attribute] = $value;
179
            }
180
        }
181
182
        return [
183
            $old,
184
            [],
185
        ];
186
    }
187
188
    /**
189
     * Get the old/new attributes of a restored event.
190
     *
191
     * @return array
192
     */
193
    protected function getRestoredEventAttributes(): array
194
    {
195
        // A restored event is just a deleted event in reverse
196
        return array_reverse($this->getDeletedEventAttributes());
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 12
    public function readyForAuditing(): bool
203
    {
204 12
        if (static::$auditingDisabled) {
205
            return false;
206
        }
207
208 12
        return $this->isEventAuditable($this->auditEvent);
209
    }
210
211
    /**
212
     * Modify attribute value.
213
     *
214
     * @param string $attribute
215
     * @param mixed  $value
216
     *
217
     * @throws AuditingException
218
     *
219
     * @return mixed
220
     */
221
    protected function modifyAttributeValue(string $attribute, $value)
222
    {
223
        $attributeModifiers = $this->getAttributeModifiers();
224
225
        if (!array_key_exists($attribute, $attributeModifiers)) {
226
            return $value;
227
        }
228
229
        $attributeModifier = $attributeModifiers[$attribute];
230
231
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
232
            return call_user_func([$attributeModifier, 'redact'], $value);
233
        }
234
235
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
236
            return call_user_func([$attributeModifier, 'encode'], $value);
237
        }
238
239
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 12
    public function toAudit(): array
246
    {
247 12
        if (!$this->readyForAuditing()) {
248
            throw new AuditingException('A valid audit event has not been set');
249
        }
250
251 12
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
252
253 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

253
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
254
            throw new AuditingException(sprintf(
255
                'Unable to handle "%s" event, %s() method missing',
256
                $this->auditEvent,
257
                $attributeGetter
258
            ));
259
        }
260
261 12
        $this->resolveAuditExclusions();
262
263 12
        list($old, $new) = $this->$attributeGetter();
264
265 12
        if ($this->getAttributeModifiers()) {
266
            foreach ($old as $attribute => $value) {
267
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
268
            }
269
270
            foreach ($new as $attribute => $value) {
271
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
272
            }
273
        }
274
275 12
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
276
277 12
        $tags = implode(',', $this->generateTags());
278
279 12
        $user = $this->resolveUser();
280
281 12
        return $this->transformAudit([
282 12
            'old_values'         => $old,
283 12
            'new_values'         => $new,
284 12
            'event'              => $this->auditEvent,
285 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

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

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

591
        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

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

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

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