Auditable::transformAudit()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

85
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
86 15
            Config::get('audit.implementation', Models\Audit::class),
87 15
            'auditable'
88
        );
89
    }
90
91
    /**
92
     * Resolve the Auditable attributes to exclude from the Audit.
93
     *
94
     * @return void
95 15
     */
96
    protected function resolveAuditExclusions()
97 15
    {
98
        $this->excludedAttributes = $this->getAuditExclude();
99
100 15
        // When in strict mode, hidden and non visible attributes are excluded
101
        if ($this->getAuditStrict()) {
102
            // Hidden attributes
103
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
104
105
            // Non visible attributes
106
            if ($this->visible) {
107
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
108
109
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
110
            }
111
        }
112
113 15
        // Exclude Timestamps
114 15
        if (!$this->getAuditTimestamps()) {
115 15
            if ($this->getCreatedAtColumn()) {
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

115
            if ($this->/** @scrutinizer ignore-call */ getCreatedAtColumn()) {
Loading history...
116
                $this->excludedAttributes[] = $this->getCreatedAtColumn();
117 15
            }
118 15
            if ($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

118
            if ($this->/** @scrutinizer ignore-call */ getUpdatedAtColumn()) {
Loading history...
119
                $this->excludedAttributes[] = $this->getUpdatedAtColumn();
120 15
            }
121 7
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
122
                $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

122
                /** @scrutinizer ignore-call */ 
123
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
123
            }
124
        }
125
126 15
        // Valid attributes are all those that made it out of the exclusion array
127
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
128 15
129
        foreach ($attributes as $attribute => $value) {
130
            // Apart from null, non scalar values will be excluded
131 15
            if (
132 15
                (is_array($value) && !Config::get('audit.allowed_array_values', false)) ||
133 15
                (is_object($value) &&
134 15
                    !method_exists($value, '__toString') &&
135
                    !($value instanceof \UnitEnum))
0 ignored issues
show
Bug introduced by
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
136
            ) {
137
                $this->excludedAttributes[] = $attribute;
138
            }
139
        }
140
    }
141
142
    /**
143
     * @return array
144 15
     */
145
    public function getAuditExclude(): array
146 15
    {
147
        return $this->auditExclude ?? Config::get('audit.exclude', []);
148
    }
149
150
    /**
151
     * @return array
152 14
     */
153
    public function getAuditInclude(): array
154 14
    {
155
        return $this->auditInclude ?? [];
156
    }
157
158
    /**
159
     * Get the old/new attributes of a retrieved event.
160
     *
161
     * @return array
162 3
     */
163
    protected function getRetrievedEventAttributes(): array
164
    {
165
        // This is a read event with no attribute changes,
166
        // only metadata will be stored in the Audit
167 3
168 3
        return [
169 3
            [],
170 3
            [],
171
        ];
172
    }
173
174
    /**
175
     * Get the old/new attributes of a created event.
176
     *
177
     * @return array
178 9
     */
179
    protected function getCreatedEventAttributes(): array
180 9
    {
181
        $new = [];
182 9
183 9
        foreach ($this->attributes as $attribute => $value) {
184 9
            if ($this->isAttributeAuditable($attribute)) {
185
                $new[$attribute] = $value;
186
            }
187
        }
188 9
189 9
        return [
190 9
            [],
191 9
            $new,
192
        ];
193
    }
194
195
    protected function getCustomEventAttributes(): array
196
    {
197
        return [
198
            $this->auditCustomOld,
199
            $this->auditCustomNew
200
        ];
201
    }
202
203
    /**
204
     * Get the old/new attributes of an updated event.
205
     *
206
     * @return array
207 3
     */
208
    protected function getUpdatedEventAttributes(): array
209 3
    {
210 3
        $old = [];
211
        $new = [];
212 3
213 3
        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

213
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
214 3
            if ($this->isAttributeAuditable($attribute)) {
215 3
                $old[$attribute] = Arr::get($this->original, $attribute);
216
                $new[$attribute] = Arr::get($this->attributes, $attribute);
217
            }
218
        }
219 3
220 3
        return [
221 3
            $old,
222 3
            $new,
223
        ];
224
    }
225
226
    /**
227
     * Get the old/new attributes of a deleted event.
228
     *
229
     * @return array
230 2
     */
231
    protected function getDeletedEventAttributes(): array
232 2
    {
233
        $old = [];
234 2
235 2
        foreach ($this->attributes as $attribute => $value) {
236 2
            if ($this->isAttributeAuditable($attribute)) {
237
                $old[$attribute] = $value;
238
            }
239
        }
240 2
241 2
        return [
242 2
            $old,
243 2
            [],
244
        ];
245
    }
246
247
    /**
248
     * Get the old/new attributes of a restored event.
249
     *
250
     * @return array
251 1
     */
252
    protected function getRestoredEventAttributes(): array
253
    {
254 1
        // A restored event is just a deleted event in reverse
255
        return array_reverse($this->getDeletedEventAttributes());
256
    }
257
258
    /**
259
     * {@inheritdoc}
260 15
     */
261
    public function readyForAuditing(): bool
262 15
    {
263
        if (static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled) {
264
            return false;
265
        }
266 15
267
        if ($this->isCustomEvent) {
268
            return true;
269
        }
270 15
271
        return $this->isEventAuditable($this->auditEvent);
272
    }
273
274
    /**
275
     * Modify attribute value.
276
     *
277
     * @param string $attribute
278
     * @param mixed $value
279
     *
280
     * @return mixed
281
     * @throws AuditingException
282
     *
283
     */
284
    protected function modifyAttributeValue(string $attribute, $value)
285
    {
286
        $attributeModifiers = $this->getAttributeModifiers();
287
288
        if (!array_key_exists($attribute, $attributeModifiers)) {
289
            return $value;
290
        }
291
292
        $attributeModifier = $attributeModifiers[$attribute];
293
294
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
295
            return call_user_func([$attributeModifier, 'redact'], $value);
296
        }
297
298
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
299
            return call_user_func([$attributeModifier, 'encode'], $value);
300
        }
301
302
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
303
    }
304
305
    /**
306
     * {@inheritdoc}
307 15
     */
308
    public function toAudit(): array
309 15
    {
310
        if (!$this->readyForAuditing()) {
311
            throw new AuditingException('A valid audit event has not been set');
312
        }
313 15
314
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
315 15
316
        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

316
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
317
            throw new AuditingException(sprintf(
318
                'Unable to handle "%s" event, %s() method missing',
319
                $this->auditEvent,
320
                $attributeGetter
321
            ));
322
        }
323 15
324
        $this->resolveAuditExclusions();
325 15
326
        list($old, $new) = $this->$attributeGetter();
327 15
328
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
329
            foreach ($old as $attribute => $value) {
330
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
331
            }
332
333
            foreach ($new as $attribute => $value) {
334
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
335
            }
336
        }
337 15
338
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
339 15
340
        $tags = implode(',', $this->generateTags());
341 15
342
        $user = $this->resolveUser();
343 15
344 15
        return $this->transformAudit(array_merge([
345 15
            'old_values'           => $old,
346 15
            'new_values'           => $new,
347 15
            'event'                => $this->auditEvent,
348 15
            '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

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

349
            'auditable_type'       => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
350 15
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
351 15
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
352 15
            'tags'                 => empty($tags) ? null : $tags,
353
        ], $this->runResolvers()));
354
    }
355
356
    /**
357
     * {@inheritdoc}
358 15
     */
359
    public function transformAudit(array $data): array
360 15
    {
361
        return $data;
362
    }
363
364
    /**
365
     * Resolve the User.
366
     *
367
     * @return mixed|null
368
     * @throws AuditingException
369
     *
370 15
     */
371
    protected function resolveUser()
372 15
    {
373
        if (!empty($this->preloadedResolverData['user'] ?? null)) {
374
            return $this->preloadedResolverData['user'];
375
        }
376 15
377
        $userResolver = Config::get('audit.user.resolver');
378 15
379
        if (is_null($userResolver) && Config::has('audit.resolver') && !Config::has('audit.user.resolver')) {
380
            trigger_error(
381
                'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html',
382
                E_USER_DEPRECATED
383
            );
384
            $userResolver = Config::get('audit.resolver.user');
385
        }
386 15
387 15
        if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
388
            return call_user_func([$userResolver, 'resolve'], $this);
389
        }
390
391
        throw new AuditingException('Invalid UserResolver implementation');
392
    }
393 15
394
    protected function runResolvers(): array
395 15
    {
396 15
        $resolved = [];
397 15
        $resolvers = Config::get('audit.resolvers', []);
398
        if (empty($resolvers) && Config::has('audit.resolver')) {
399
            trigger_error(
400
                'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html',
401
                E_USER_DEPRECATED
402
            );
403
            $resolvers = Config::get('audit.resolver', []);
404
        }
405 15
406 15
        foreach ($resolvers as $name => $implementation) {
407
            if (empty($implementation)) {
408
                continue;
409
            }
410 15
411
            if (!is_subclass_of($implementation, Resolver::class)) {
412
                throw new AuditingException('Invalid Resolver implementation for: ' . $name);
413 15
            }
414
            $resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
415 15
        }
416
        return $resolved;
417
    }
418 15
419
    public function preloadResolverData()
420 15
    {
421
        $this->preloadedResolverData = $this->runResolvers();
422 15
423 15
        $user = $this->resolveUser();
424
        if (!empty($user)) {
425
            $this->preloadedResolverData['user'] = $user;
426
        }
427 15
428
        return $this;
429
    }
430
431
    /**
432
     * Determine if an attribute is eligible for auditing.
433
     *
434
     * @param string $attribute
435
     *
436
     * @return bool
437 14
     */
438
    protected function isAttributeAuditable(string $attribute): bool
439
    {
440 14
        // The attribute should not be audited
441 13
        if (in_array($attribute, $this->excludedAttributes, true)) {
442
            return false;
443
        }
444
445
        // The attribute is auditable when explicitly
446 14
        // listed or when the include array is empty
447
        $include = $this->getAuditInclude();
448 14
449
        return empty($include) || in_array($attribute, $include, true);
450
    }
451
452
    /**
453
     * Determine whether an event is auditable.
454
     *
455
     * @param string $event
456
     *
457
     * @return bool
458 15
     */
459
    protected function isEventAuditable($event): bool
460 15
    {
461
        return is_string($this->resolveAttributeGetter($event));
462
    }
463
464
    /**
465
     * Attribute getter method resolver.
466
     *
467
     * @param string $event
468
     *
469
     * @return string|null
470 15
     */
471
    protected function resolveAttributeGetter($event)
472 15
    {
473 8
        if (empty($event)) {
474
            return;
475
        }
476 15
477
        if ($this->isCustomEvent) {
478
            return 'getCustomEventAttributes';
479
        }
480 15
481 15
        foreach ($this->getAuditEvents() as $key => $value) {
482
            $auditableEvent = is_int($key) ? $value : $key;
483 15
484
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
485 15
486 15
            if (preg_match($auditableEventRegex, $event)) {
487
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
488
            }
489
        }
490
    }
491
492
    /**
493
     * {@inheritdoc}
494 15
     */
495
    public function setAuditEvent(string $event): Contracts\Auditable
496 15
    {
497
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
498 15
499
        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...
500
    }
501
502
    /**
503
     * {@inheritdoc}
504 15
     */
505
    public function getAuditEvent()
506 15
    {
507
        return $this->auditEvent;
508
    }
509
510
    /**
511
     * {@inheritdoc}
512 15
     */
513
    public function getAuditEvents(): array
514 15
    {
515 15
        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...
516 15
            'created',
517 15
            'updated',
518 15
            'deleted',
519 15
            'restored',
520
        ]);
521
    }
522
523
    /**
524
     * Is Auditing disabled.
525
     *
526
     * @return bool
527
     */
528
    public static function isAuditingDisabled(): bool
529
    {
530
        return static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled;
531
    }
532
533
    /**
534
     * Disable Auditing.
535
     *
536
     * @return void
537
     */
538
    public static function disableAuditing()
539
    {
540
        static::$auditingDisabled = true;
541
    }
542
543
    /**
544
     * Enable Auditing.
545
     *
546
     * @return void
547
     */
548
    public static function enableAuditing()
549
    {
550
        static::$auditingDisabled = false;
551
    }
552
553
    /**
554
     * Execute a callback while auditing is disabled.
555
     *
556
     * @param callable $callback
557
     * @param bool $globally
558
     *
559
     * @return mixed
560
     */
561
    public static function withoutAuditing(callable $callback, bool $globally = false)
562
    {
563
        $auditingDisabled = static::$auditingDisabled;
564
565
        static::disableAuditing();
566
        Models\Audit::$auditingGloballyDisabled = $globally;
567
568
        try {
569
            return $callback();
570
        } finally {
571
            Models\Audit::$auditingGloballyDisabled = false;
572
            static::$auditingDisabled = $auditingDisabled;
573
        }
574
    }
575
576
    /**
577
     * Determine whether auditing is enabled.
578
     *
579
     * @return bool
580 17
     */
581
    public static function isAuditingEnabled(): bool
582 17
    {
583 15
        if (App::runningInConsole()) {
584
            return Config::get('audit.enabled', true) && Config::get('audit.console', false);
585
        }
586 2
587
        return Config::get('audit.enabled', true);
588
    }
589
590
    /**
591
     * {@inheritdoc}
592 15
     */
593
    public function getAuditStrict(): bool
594 15
    {
595
        return $this->auditStrict ?? Config::get('audit.strict', false);
596
    }
597
598
    /**
599
     * {@inheritdoc}
600 15
     */
601
    public function getAuditTimestamps(): bool
602 15
    {
603
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
604
    }
605
606
    /**
607
     * {@inheritdoc}
608 15
     */
609
    public function getAuditDriver()
610 15
    {
611
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
612
    }
613
614
    /**
615
     * {@inheritdoc}
616 15
     */
617
    public function getAuditThreshold(): int
618 15
    {
619
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
620
    }
621
622
    /**
623
     * {@inheritdoc}
624 15
     */
625
    public function getAttributeModifiers(): array
626 15
    {
627
        return $this->attributeModifiers ?? [];
628
    }
629
630
    /**
631
     * {@inheritdoc}
632 15
     */
633
    public function generateTags(): array
634 15
    {
635
        return [];
636
    }
637
638
    /**
639
     * {@inheritdoc}
640
     */
641
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
642
    {
643
        // The Audit must be for an Auditable model of this type
644
        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...
645
            throw new AuditableTransitionException(sprintf(
646
                'Expected Auditable type %s, got %s instead',
647
                $this->getMorphClass(),
648
                $audit->auditable_type
649
            ));
650
        }
651
652
        // The Audit must be for this specific Auditable model
653
        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...
654
            throw new AuditableTransitionException(sprintf(
655
                'Expected Auditable id (%s)%s, got (%s)%s instead',
656
                gettype($this->getKey()),
657
                $this->getKey(),
658
                gettype($audit->auditable_id),
659
                $audit->auditable_id
660
            ));
661
        }
662
663
        // Redacted data should not be used when transitioning states
664
        foreach ($this->getAttributeModifiers() as $attribute => $modifier) {
665
            if (is_subclass_of($modifier, AttributeRedactor::class)) {
666
                throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set');
667
            }
668
        }
669
670
        // The attribute compatibility between the Audit and the Auditable model must be met
671
        $modified = $audit->getModified();
672
673
        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

673
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
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

673
        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...
674
            throw new AuditableTransitionException(sprintf(
675
                'Incompatibility between [%s:%s] and [%s:%s]',
676
                $this->getMorphClass(),
677
                $this->getKey(),
678
                get_class($audit),
679
                $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

679
                $audit->/** @scrutinizer ignore-call */ 
680
                        getKey()
Loading history...
680
            ), array_keys($incompatibilities));
681
        }
682
683
        $key = $old ? 'old' : 'new';
684
685
        foreach ($modified as $attribute => $value) {
686
            if (array_key_exists($key, $value)) {
687
                $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

687
                $this->/** @scrutinizer ignore-call */ 
688
                       setAttribute($attribute, $value[$key]);
Loading history...
688
            }
689
        }
690
691
        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...
692
    }
693
694
    /*
695
    |--------------------------------------------------------------------------
696
    | Pivot help methods
697
    |--------------------------------------------------------------------------
698
    |
699
    | Methods for auditing pivot actions
700
    |
701
    */
702
703
    /**
704
     * @param string $relationName
705
     * @param mixed $id
706
     * @param array $attributes
707
     * @param bool $touch
708
     * @param array $columns
709
     * @param \Closure|null $callback
710
     * @return void
711
     * @throws AuditingException
712
     */
713
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'], $callback = null)
714
    {
715
        $this->validateRelationshipMethodExistence($relationName, 'attach');
716
717
        $relationCall = $this->{$relationName}();
718
719
        if ($callback instanceof \Closure) {
720
            $this->applyClosureToRelationship($relationCall, $callback);
721
        }
722
723
        $old = $relationCall->get($columns);
724
        $relationCall->attach($id, $attributes, $touch);
725
        $new = $relationCall->get($columns);
726
727
        $this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new);
728
    }
729
730
    /**
731
     * @param string $relationName
732
     * @param mixed $ids
733
     * @param bool $touch
734
     * @param array $columns
735
     * @param \Closure|null $callback
736
     * @return int
737
     * @throws AuditingException
738
     */
739
    public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'], $callback = null)
740
    {
741
        $this->validateRelationshipMethodExistence($relationName, 'detach');
742
743
        $relationCall = $this->{$relationName}();
744
745
        if ($callback instanceof \Closure) {
746
            $this->applyClosureToRelationship($relationCall, $callback);
747
        }
748
749
        $old = $relationCall->get($columns);
750
        $results = $relationCall->detach($ids, $touch);
751
        $new = $relationCall->get($columns);
752
753
        $this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new);
754
755
        return empty($results) ? 0 : $results;
756
    }
757
758
    /**
759
     * @param string $relationName
760
     * @param Collection|Model|array $ids
761
     * @param bool $detaching
762
     * @param array $columns
763
     * @param \Closure|null $callback
764
     * @return array
765
     * @throws AuditingException
766
     */
767
    public function auditSync(string $relationName, $ids, $detaching = true, $columns = ['*'], $callback = null)
768
    {
769
        $this->validateRelationshipMethodExistence($relationName, 'sync');
770
771
        $relationCall = $this->{$relationName}();
772
773
        if ($callback instanceof \Closure) {
774
            $this->applyClosureToRelationship($relationCall, $callback);
775
        }
776
777
        $old = $relationCall->get($columns);
778
        $changes = $relationCall->sync($ids, $detaching);
779
780
        if (collect($changes)->flatten()->isEmpty()) {
781
            $old = $new = collect([]);
782
        } else {
783
            $new = $relationCall->get($columns);
784
        }
785
786
        $this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new);
787
788
        return $changes;
789
    }
790
791
    /**
792
     * @param string $relationName
793
     * @param Collection|Model|array $ids
794
     * @param array $columns
795
     * @param \Closure|null $callback
796
     * @return array
797
     * @throws AuditingException
798
     */
799
    public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'], $callback = null)
800
    {
801
        $this->validateRelationshipMethodExistence($relationName, 'syncWithoutDetaching');
802
803
        return $this->auditSync($relationName, $ids, false, $columns, $callback);
804
    }
805
806
    /**
807
     * @param string $relationName
808
     * @param Collection|Model|array  $ids
809
     * @param array  $values
810
     * @param bool  $detaching
811
     * @param array $columns
812
     * @param \Closure|null $callback
813
     * @return array
814
     */
815
    public function auditSyncWithPivotValues(string $relationName, $ids, array $values, bool $detaching = true, $columns = ['*'], $callback = null)
816
    {
817
        $this->validateRelationshipMethodExistence($relationName, 'syncWithPivotValues');
818
819
        if ($ids instanceof Model) {
820
            $ids = $ids->getKey();
821
        } elseif ($ids instanceof \Illuminate\Database\Eloquent\Collection) {
822
            $ids = $ids->isEmpty() ? [] : $ids->pluck($ids->first()->getKeyName())->toArray();
823
        } elseif ($ids instanceof Collection) {
824
            $ids = $ids->toArray();
825
        }
826
827
        return $this->auditSync($relationName, collect(Arr::wrap($ids))->mapWithKeys(function ($id) use ($values) {
828
            return [$id => $values];
829
        }), $detaching, $columns, $callback);
830
    }
831
832
    /**
833
     * @param string $relationName
834
     * @param string $event
835
     * @param Collection $old
836
     * @param Collection $new
837
     * @return void
838
     */
839
    private function dispatchRelationAuditEvent($relationName, $event, $old, $new)
840
    {
841
        $this->auditCustomOld[$relationName] = $old->diff($new)->toArray();
842
        $this->auditCustomNew[$relationName] = $new->diff($old)->toArray();
843
844
        if (
845
            empty($this->auditCustomOld[$relationName]) &&
846
            empty($this->auditCustomNew[$relationName])
847
        ) {
848
            $this->auditCustomOld = $this->auditCustomNew = [];
849
        }
850
851
        $this->auditEvent = $event;
852
        $this->isCustomEvent = true;
853
        Event::dispatch(new AuditCustom($this));
0 ignored issues
show
Bug introduced by
$this of type OwenIt\Auditing\Auditable is incompatible with the type OwenIt\Auditing\Contracts\Auditable expected by parameter $model of OwenIt\Auditing\Events\AuditCustom::__construct(). ( Ignorable by Annotation )

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

853
        Event::dispatch(new AuditCustom(/** @scrutinizer ignore-type */ $this));
Loading history...
854
        $this->auditCustomOld = $this->auditCustomNew = [];
855
        $this->isCustomEvent = false;
856
    }
857
858
    private function validateRelationshipMethodExistence(string $relationName, string $methodName): void
859
    {
860
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), $methodName)) {
861
            throw new AuditingException("Relationship $relationName was not found or does not support method $methodName");
862
        }
863
    }
864
865
    private function applyClosureToRelationship(BelongsToMany $relation, \Closure $closure): void
866
    {
867
        try {
868
            $closure($relation);
869
        } catch (\Throwable $exception) {
870
            throw new AuditingException("Invalid Closure for {$relation->getRelationName()} Relationship");
871
        }
872
    }
873
}
874