Passed
Pull Request — master (#684)
by Morten
09:04
created

Auditable::getAuditTimestamps()   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 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
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\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
     * Auditable boot logic.
46
     *
47
     * @return void
48
     */
49 196
    public static function bootAuditable()
50
    {
51 196
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
52 192
            static::observe(new AuditableObserver());
53
        }
54 196
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59 32
    public function audits(): MorphMany
60
    {
61 32
        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

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

91
            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

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

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

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

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

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

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

561
        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

561
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
562 2
            throw new AuditableTransitionException(sprintf(
563 2
                'Incompatibility between [%s:%s] and [%s:%s]',
564 2
                $this->getMorphClass(),
565 2
                $this->getKey(),
566 2
                get_class($audit),
567 2
                $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

567
                $audit->/** @scrutinizer ignore-call */ 
568
                        getKey()
Loading history...
568 2
            ), array_keys($incompatibilities));
569
        }
570
571 10
        $key = $old ? 'old' : 'new';
572
573 10
        foreach ($modified as $attribute => $value) {
574 6
            if (array_key_exists($key, $value)) {
575 6
                $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

575
                $this->/** @scrutinizer ignore-call */ 
576
                       setAttribute($attribute, $value[$key]);
Loading history...
576
            }
577
        }
578
579 10
        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...
580
    }
581
}
582