Passed
Pull Request — master (#684)
by Morten
08:05
created

Auditable   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 676
Duplicated Lines 0 %

Test Coverage

Coverage 87.45%

Importance

Changes 39
Bugs 16 Features 5
Metric Value
wmc 103
eloc 204
c 39
b 16
f 5
dl 0
loc 676
ccs 202
cts 231
cp 0.8745
rs 2

35 Methods

Rating   Name   Duplication   Size   Complexity  
A disableAuditing() 0 3 1
B resolveAttributeGetter() 0 17 7
A getAuditStrict() 0 3 1
B resolveAuditExclusions() 0 33 9
A getUpdatedEventAttributes() 0 15 3
A getRetrievedEventAttributes() 0 8 1
A getRestoredEventAttributes() 0 4 1
A getAuditEvents() 0 7 1
A modifyAttributeValue() 0 19 4
A getCreatedEventAttributes() 0 13 3
A runResolvers() 0 14 4
A getAuditEvent() 0 3 1
A getAuditTimestamps() 0 3 1
A isEventAuditable() 0 3 1
A bootAuditable() 0 4 3
A auditSync() 0 17 5
A transformAudit() 0 3 1
A getCustomEventAttributes() 0 5 1
A readyForAuditing() 0 11 3
B toAudit() 0 46 10
A getAuditThreshold() 0 3 1
A generateTags() 0 3 1
A getAttributeModifiers() 0 3 1
A resolveUser() 0 9 2
A enableAuditing() 0 3 1
B transitionTo() 0 49 9
A getAuditDriver() 0 3 1
A audits() 0 5 1
A auditDetach() 0 17 6
A isAuditingEnabled() 0 7 3
A setAuditEvent() 0 5 2
A isAttributeAuditable() 0 12 3
A auditAttach() 0 15 5
A auditSyncWithoutDetaching() 0 6 3
A getDeletedEventAttributes() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like Auditable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Auditable, and based on these observations, apply Extract Interface, too.

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\Concerns\ExcludesAuditAttributes;
12
use OwenIt\Auditing\Concerns\IncludesAuditAttribute;
13
use OwenIt\Auditing\Contracts\AttributeEncoder;
14
use OwenIt\Auditing\Contracts\AttributeRedactor;
15
use OwenIt\Auditing\Contracts\Resolver;
16
use OwenIt\Auditing\Events\AuditCustom;
17
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
18
use OwenIt\Auditing\Exceptions\AuditingException;
19
20
trait Auditable
21
{
22 1
    use ExcludesAuditAttributes;
23 1
    use IncludesAuditAttribute;
24
25
26
    /**
27
     * Auditable attributes excluded from the Audit.
28
     *
29
     * @var array
30
     */
31
    protected $excludedAttributes = [];
32
33
    /**
34
     * Audit event name.
35
     *
36
     * @var string
37
     */
38
    protected $auditEvent;
39
40
    /**
41
     * Is auditing disabled?
42
     *
43
     * @var bool
44
     */
45
    public static $auditingDisabled = false;
46
47
    /**
48
     * Property may set custom event data to register
49
     * @var null|array
50
     */
51
    public $auditCustomOld = null;
52
53
    /**
54
     * Property may set custom event data to register
55
     * @var null|array
56
     */
57
    public $auditCustomNew = null;
58
59
    /**
60
     * If this is a custom event (as opposed to an eloquent event
61
     * @var bool
62
     */
63
    public $isCustomEvent = false;
64
65
    /**
66
     * Auditable boot logic.
67
     *
68
     * @return void
69
     */
70 204
    public static function bootAuditable()
71
    {
72 204
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
73 200
            static::observe(new AuditableObserver());
74
        }
75 204
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 40
    public function audits(): MorphMany
81
    {
82 40
        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 40
            Config::get('audit.implementation', Models\Audit::class),
84 40
            'auditable'
85
        );
86
    }
87
88
    /**
89
     * Resolve the Auditable attributes to exclude from the Audit.
90
     *
91
     * @return void
92
     */
93 132
    protected function resolveAuditExclusions()
94
    {
95 132
        $this->excludedAttributes = $this->getAuditExclude();
96
97
        // When in strict mode, hidden and non visible attributes are excluded
98 132
        if ($this->getAuditStrict()) {
99
            // Hidden attributes
100 2
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
101
102
            // Non visible attributes
103 2
            if ($this->visible) {
104 2
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
105
106 2
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
107
            }
108
        }
109
110
        // Exclude Timestamps
111 132
        if (!$this->getAuditTimestamps()) {
112 132
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $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

112
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
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
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
113
114 132
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
115 124
                $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

115
                /** @scrutinizer ignore-call */ 
116
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
Loading history...
116
            }
117
        }
118
119
        // Valid attributes are all those that made it out of the exclusion array
120 132
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
121
122 132
        foreach ($attributes as $attribute => $value) {
123
            // Apart from null, non scalar values will be excluded
124 124
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
125 4
                $this->excludedAttributes[] = $attribute;
126
            }
127
        }
128 132
    }
129
130
    /**
131
     * Get the old/new attributes of a retrieved event.
132
     *
133
     * @return array
134
     */
135 6
    protected function getRetrievedEventAttributes(): array
136
    {
137
        // This is a read event with no attribute changes,
138
        // only metadata will be stored in the Audit
139
140
        return [
141 6
            [],
142
            [],
143
        ];
144
    }
145
146
    /**
147
     * Get the old/new attributes of a created event.
148
     *
149
     * @return array
150
     */
151 118
    protected function getCreatedEventAttributes(): array
152
    {
153 118
        $new = [];
154
155 118
        foreach ($this->attributes as $attribute => $value) {
156 110
            if ($this->isAttributeAuditable($attribute)) {
157 110
                $new[$attribute] = $value;
158
            }
159
        }
160
161
        return [
162 118
            [],
163 118
            $new,
164
        ];
165
    }
166
167 2
    protected function getCustomEventAttributes(): array
168
    {
169
        return [
170 2
            $this->auditCustomOld,
171 2
            $this->auditCustomNew
172
        ];
173
    }
174
175
    /**
176
     * Get the old/new attributes of an updated event.
177
     *
178
     * @return array
179
     */
180 24
    protected function getUpdatedEventAttributes(): array
181
    {
182 24
        $old = [];
183 24
        $new = [];
184
185 24
        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

185
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
186 22
            if ($this->isAttributeAuditable($attribute)) {
187 18
                $old[$attribute] = Arr::get($this->original, $attribute);
188 18
                $new[$attribute] = Arr::get($this->attributes, $attribute);
189
            }
190
        }
191
192
        return [
193 24
            $old,
194 24
            $new,
195
        ];
196
    }
197
198
    /**
199
     * Get the old/new attributes of a deleted event.
200
     *
201
     * @return array
202
     */
203 8
    protected function getDeletedEventAttributes(): array
204
    {
205 8
        $old = [];
206
207 8
        foreach ($this->attributes as $attribute => $value) {
208 8
            if ($this->isAttributeAuditable($attribute)) {
209 8
                $old[$attribute] = $value;
210
            }
211
        }
212
213
        return [
214 8
            $old,
215
            [],
216
        ];
217
    }
218
219
    /**
220
     * Get the old/new attributes of a restored event.
221
     *
222
     * @return array
223
     */
224 4
    protected function getRestoredEventAttributes(): array
225
    {
226
        // A restored event is just a deleted event in reverse
227 4
        return array_reverse($this->getDeletedEventAttributes());
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 154
    public function readyForAuditing(): bool
234
    {
235 154
        if (static::$auditingDisabled) {
236 2
            return false;
237
        }
238
239 154
        if ($this->isCustomEvent) {
240 2
            return true;
241
        }
242
243 154
        return $this->isEventAuditable($this->auditEvent);
244
    }
245
246
    /**
247
     * Modify attribute value.
248
     *
249
     * @param string $attribute
250
     * @param mixed $value
251
     *
252
     * @return mixed
253
     * @throws AuditingException
254
     *
255
     */
256 4
    protected function modifyAttributeValue(string $attribute, $value)
257
    {
258 4
        $attributeModifiers = $this->getAttributeModifiers();
259
260 4
        if (!array_key_exists($attribute, $attributeModifiers)) {
261 2
            return $value;
262
        }
263
264 4
        $attributeModifier = $attributeModifiers[$attribute];
265
266 4
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
267 2
            return call_user_func([$attributeModifier, 'redact'], $value);
268
        }
269
270 4
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
271 2
            return call_user_func([$attributeModifier, 'encode'], $value);
272
        }
273
274 2
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280 142
    public function toAudit(): array
281
    {
282 142
        if (!$this->readyForAuditing()) {
283 2
            throw new AuditingException('A valid audit event has not been set');
284
        }
285
286 140
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
287
288 140
        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

288
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
289 8
            throw new AuditingException(sprintf(
290 8
                'Unable to handle "%s" event, %s() method missing',
291 8
                $this->auditEvent,
292
                $attributeGetter
293
            ));
294
        }
295
296 132
        $this->resolveAuditExclusions();
297
298 132
        list($old, $new) = $this->$attributeGetter();
299
300 132
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
301 4
            foreach ($old as $attribute => $value) {
302 2
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
303
            }
304
305 4
            foreach ($new as $attribute => $value) {
306 4
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
307
            }
308
        }
309
310 130
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
311
312 130
        $tags = implode(',', $this->generateTags());
313
314 130
        $user = $this->resolveUser();
315
316 128
        return $this->transformAudit(array_merge([
317 128
            'old_values'           => $old,
318 128
            'new_values'           => $new,
319 128
            'event'                => $this->auditEvent,
320 128
            '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

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

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

577
        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

577
        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...
578 2
            throw new AuditableTransitionException(sprintf(
579 2
                'Incompatibility between [%s:%s] and [%s:%s]',
580 2
                $this->getMorphClass(),
581 2
                $this->getKey(),
582 2
                get_class($audit),
583 2
                $audit->getKey()
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on OwenIt\Auditing\Contracts\Audit. Since it exists in all sub-types, consider adding an abstract or default implementation to OwenIt\Auditing\Contracts\Audit. ( Ignorable by Annotation )

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

583
                $audit->/** @scrutinizer ignore-call */ 
584
                        getKey()
Loading history...
584 2
            ), array_keys($incompatibilities));
585
        }
586
587 10
        $key = $old ? 'old' : 'new';
588
589 10
        foreach ($modified as $attribute => $value) {
590 6
            if (array_key_exists($key, $value)) {
591 6
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

591
                $this->/** @scrutinizer ignore-call */ 
592
                       setAttribute($attribute, $value[$key]);
Loading history...
592
            }
593
        }
594
595 10
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type OwenIt\Auditing\Auditable which is incompatible with the type-hinted return OwenIt\Auditing\Contracts\Auditable.
Loading history...
596
    }
597
598
    /*
599
    |--------------------------------------------------------------------------
600
    | Pivot help methods
601
    |--------------------------------------------------------------------------
602
    |
603
    | Methods for auditing pivot actions
604
    |
605
    */
606
607
    /**
608
     * @param string $relationName
609
     * @param mixed $id
610
     * @param array $attributes
611
     * @param bool $touch
612
     * @return void
613
     * @throws AuditingException
614
     */
615 2
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['name'])
616
    {
617 2
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'attach')) {
618
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method attach');
619
        }
620 2
        $this->auditEvent = 'attach';
621 2
        $this->isCustomEvent = true;
622 2
        $this->auditCustomOld = [
623 2
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
624
        ];
625 2
        $this->{$relationName}()->attach($id, $attributes, $touch);
626 2
        $this->auditCustomNew = [
627 2
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
628
        ];
629 2
        Event::dispatch(AuditCustom::class, [$this]);
630 2
    }
631
632
    /**
633
     * @param string $relationName
634
     * @param mixed $ids
635
     * @param bool $touch
636
     * @return int
637
     * @throws AuditingException
638
     */
639
    public function auditDetach(string $relationName, $ids = null, $touch = true)
640
    {
641
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'detach')) {
642
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method detach');
643
        }
644
645
        $this->auditEvent = 'detach';
646
        $this->isCustomEvent = true;
647
        $this->auditCustomOld = [
648
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
649
        ];
650
        $results = $this->{$relationName}()->detach($ids, $touch);
651
        $this->auditCustomNew = [
652
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
653
        ];
654
        Event::dispatch(AuditCustom::class, [$this]);
655
        return empty($results) ? 0 : $results;
656
    }
657
658
    /**
659
     * @param $relationName
660
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
661
     * @param bool $detaching
662
     * @return array
663
     * @throws AuditingException
664
     */
665
    public function auditSync($relationName, $ids, $detaching = true)
666
    {
667
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
668
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method sync');
669
        }
670
671
        $this->auditEvent = 'sync';
672
        $this->isCustomEvent = true;
673
        $this->auditCustomOld = [
674
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
675
        ];
676
        $changes = $this->{$relationName}()->sync($ids, $detaching);
677
        $this->auditCustomNew = [
678
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
679
        ];
680
        Event::dispatch(AuditCustom::class, [$this]);
681
        return $changes;
682
    }
683
684
    /**
685
     * @param string $relationName
686
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
687
     * @return array
688
     * @throws AuditingException
689
     */
690
    public function auditSyncWithoutDetaching(string $relationName, $ids)
691
    {
692
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'syncWithoutDetaching')) {
693
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method syncWithoutDetaching');
694
        }
695
        return $this->auditSync($relationName, $ids, false);
696
    }
697
}
698