Test Failed
Push — master ( 60de6d...c22b1e )
by
unknown
08:52
created

Auditable::resolveAuditExclusions()   C

Complexity

Conditions 13
Paths 81

Size

Total Lines 42
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 15.2812

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 13
eloc 21
c 4
b 2
f 0
nc 81
nop 0
dl 0
loc 42
ccs 16
cts 21
cp 0.7619
crap 15.2812
rs 6.6166

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

670
        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

670
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
671
            throw new AuditableTransitionException(sprintf(
672
                'Incompatibility between [%s:%s] and [%s:%s]',
673
                $this->getMorphClass(),
674
                $this->getKey(),
675
                get_class($audit),
676
                $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

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

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