Test Failed
Push — master ( c22b1e...5dd403 )
by
unknown
08:33
created

Auditable::resolveAttributeGetter()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 9
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 17
ccs 9
cts 10
cp 0.9
crap 7.049
rs 8.8333
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
trait Auditable
22
{
23
    /**
24
     * Auditable attributes excluded from the Audit.
25
     *
26
     * @var array
27
     */
28
    protected $excludedAttributes = [];
29
30
    /**
31
     * Audit event name.
32
     *
33
     * @var string
34
     */
35
    public $auditEvent;
36
37
    /**
38
     * Is auditing disabled?
39
     *
40
     * @var bool
41
     */
42
    public static $auditingDisabled = false;
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
     * @var array Preloaded data to be used by resolvers
64
     */
65
    public $preloadedResolverData = [];
66
67
    /**
68
     * Auditable boot logic.
69
     *
70
     * @return void
71
     */
72 17
    public static function bootAuditable()
73
    {
74 17
        if (static::isAuditingEnabled()) {
75 15
            static::observe(new AuditableObserver());
76
        }
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 15
    public function audits(): MorphMany
83
    {
84 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

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

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

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

121
                /** @scrutinizer ignore-call */ 
122
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
122
            }
123
        }
124
125
        // Valid attributes are all those that made it out of the exclusion array
126 15
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
127
128 15
        foreach ($attributes as $attribute => $value) {
129
            // Apart from null, non scalar values will be excluded
130
            if (
131 15
                (is_array($value) && !Config::get('audit.allowed_array_values', false)) ||
132 15
                (is_object($value) &&
133 15
                    !method_exists($value, '__toString') &&
134 15
                    !($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...
135
            ) {
136
                $this->excludedAttributes[] = $attribute;
137
            }
138
        }
139
    }
140
141
    /**
142
     * @return array
143
     */
144 15
    public function getAuditExclude(): array
145
    {
146 15
        return $this->auditExclude ?? Config::get('audit.exclude', []);
147
    }
148
149
    /**
150
     * @return array
151
     */
152 14
    public function getAuditInclude(): array
153
    {
154 14
        return $this->auditInclude ?? [];
155
    }
156
157
    /**
158
     * Get the old/new attributes of a retrieved event.
159
     *
160
     * @return array
161
     */
162 3
    protected function getRetrievedEventAttributes(): array
163
    {
164
        // This is a read event with no attribute changes,
165
        // only metadata will be stored in the Audit
166
167 3
        return [
168 3
            [],
169 3
            [],
170 3
        ];
171
    }
172
173
    /**
174
     * Get the old/new attributes of a created event.
175
     *
176
     * @return array
177
     */
178 9
    protected function getCreatedEventAttributes(): array
179
    {
180 9
        $new = [];
181
182 9
        foreach ($this->attributes as $attribute => $value) {
183 9
            if ($this->isAttributeAuditable($attribute)) {
184 9
                $new[$attribute] = $value;
185
            }
186
        }
187
188 9
        return [
189 9
            [],
190 9
            $new,
191 9
        ];
192
    }
193
194
    protected function getCustomEventAttributes(): array
195
    {
196
        return [
197
            $this->auditCustomOld,
198
            $this->auditCustomNew
199
        ];
200
    }
201
202
    /**
203
     * Get the old/new attributes of an updated event.
204
     *
205
     * @return array
206
     */
207 3
    protected function getUpdatedEventAttributes(): array
208
    {
209 3
        $old = [];
210 3
        $new = [];
211
212 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

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

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

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

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

672
        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

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

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

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