Test Failed
Push — master ( a8de03...7adc29 )
by Morten
16:27 queued 04:37
created

Auditable::dispatchRelationAuditEvent()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 2
nop 4
dl 0
loc 16
ccs 0
cts 4
cp 0
crap 12
rs 9.9332
c 0
b 0
f 0
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
     * Auditable boot logic.
61
     *
62
     * @return void
63
     */
64 218
    public static function bootAuditable()
65
    {
66 218
        if (static::isAuditingEnabled()) {
67 216
            static::observe(new AuditableObserver());
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 40
    public function audits(): MorphMany
75
    {
76 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

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

106
            if ($this->/** @scrutinizer ignore-call */ getCreatedAtColumn()) {
Loading history...
107 146
                $this->excludedAttributes[] = $this->getCreatedAtColumn();
108
            }
109 146
            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

109
            if ($this->/** @scrutinizer ignore-call */ getUpdatedAtColumn()) {
Loading history...
110 146
                $this->excludedAttributes[] = $this->getUpdatedAtColumn();
111
            }
112 146
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
113 138
                $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

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

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

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

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

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

615
        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

615
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
616 2
            throw new AuditableTransitionException(sprintf(
617 2
                'Incompatibility between [%s:%s] and [%s:%s]',
618 2
                $this->getMorphClass(),
619 2
                $this->getKey(),
620 2
                get_class($audit),
621 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

621
                $audit->/** @scrutinizer ignore-call */ 
622
                        getKey()
Loading history...
622 2
            ), array_keys($incompatibilities));
623
        }
624
625 12
        $key = $old ? 'old' : 'new';
626
627 12
        foreach ($modified as $attribute => $value) {
628 8
            if (array_key_exists($key, $value)) {
629 8
                $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

629
                $this->/** @scrutinizer ignore-call */ 
630
                       setAttribute($attribute, $value[$key]);
Loading history...
630
            }
631
        }
632
633 12
        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...
634
    }
635
636
    /*
637
    |--------------------------------------------------------------------------
638
    | Pivot help methods
639
    |--------------------------------------------------------------------------
640
    |
641
    | Methods for auditing pivot actions
642
    |
643
    */
644
645
    /**
646
     * @param string $relationName
647
     * @param mixed $id
648
     * @param array $attributes
649
     * @param bool $touch
650
     * @param array $columns
651
     * @return void
652
     * @throws AuditingException
653 2
     */
654
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'])
655 2
    {
656
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'attach')) {
657
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method attach');
658 2
        }
659 2
660 2
        $old = $this->{$relationName}()->get($columns);
661 2
        $this->{$relationName}()->attach($id, $attributes, $touch);
662 2
        $new = $this->{$relationName}()->get($columns);
663 2
        $this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new);
664 2
    }
665 2
666 2
    /**
667 2
     * @param string $relationName
668 2
     * @param mixed $ids
669
     * @param bool $touch
670
     * @param array $columns
671
     * @return int
672
     * @throws AuditingException
673
     */
674
    public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'])
675
    {
676
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'detach')) {
677
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method detach');
678
        }
679
680
        $old = $this->{$relationName}()->get($columns);
681
        $results = $this->{$relationName}()->detach($ids, $touch);
682
        $new = $this->{$relationName}()->get($columns);
683
        $this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new);
684
685
        return empty($results) ? 0 : $results;
686
    }
687
688
    /**
689
     * @param $relationName
690
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
691
     * @param bool $detaching
692
     * @param array $columns
693
     * @return array
694
     * @throws AuditingException
695
     */
696
    public function auditSync($relationName, $ids, $detaching = true, $columns = ['*'])
697
    {
698
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
699
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method sync');
700
        }
701
702
        $old = $this->{$relationName}()->get($columns);
703
        $changes = $this->{$relationName}()->sync($ids, $detaching);
704
        if (collect($changes)->flatten()->isEmpty()) {
705
            $old = $new = collect([]);
706 8
        } else {
707
            $new = $this->{$relationName}()->get($columns);
708 8
        }
709
        $this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new);
710
711
        return $changes;
712 8
    }
713
714 8
    /**
715 8
     * @param string $relationName
716 8
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
717
     * @param array $columns
718 8
     * @return array
719
     * @throws AuditingException
720 8
     */
721 4
    public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'])
722 4
    {
723
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'syncWithoutDetaching')) {
724 4
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method syncWithoutDetaching');
725 4
        }
726 4
727
        return $this->auditSync($relationName, $ids, false, $columns);
728
    }
729 8
730 8
    /**
731 8
     * @param string $relationName
732
     * @param string $event
733 8
     * @param \Illuminate\Support\Collection $old
734
     * @param \Illuminate\Support\Collection $new
735
     * @return void
736
     */
737
    private function dispatchRelationAuditEvent($relationName, $event, $old, $new)
738
    {
739
        $this->auditCustomOld[$relationName] = $old->toArray();
740
        $this->auditCustomNew[$relationName] = $new->toArray();
741
742
        if (
743
            empty($this->auditCustomOld[$relationName]) &&
744
            empty($this->auditCustomNew[$relationName])
745
        ) {
746
            $this->auditCustomOld = $this->auditCustomNew = [];
747
        }
748
749
        $this->auditEvent = $event;
750
        $this->isCustomEvent = true;
751
        Event::dispatch(AuditCustom::class, [$this]);
752
        $this->isCustomEvent = false;
753
    }
754
}
755