Test Failed
Push — master ( 0b636e...60de6d )
by
unknown
07:34
created

Auditable::getRestoredEventAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

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

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

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

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

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

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

669
        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

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

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

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