Passed
Pull Request — master (#684)
by Morten
07:50
created

Auditable::getUpdatedEventAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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

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

108
            array_push($this->excludedAttributes, $this->/** @scrutinizer ignore-call */ getCreatedAtColumn(), $this->getUpdatedAtColumn());
Loading history...
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

108
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->/** @scrutinizer ignore-call */ getUpdatedAtColumn());
Loading history...
109
110 130
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
111 122
                $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

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

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

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

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

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

581
        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

581
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
582 2
            throw new AuditableTransitionException(sprintf(
583 2
                'Incompatibility between [%s:%s] and [%s:%s]',
584 2
                $this->getMorphClass(),
585 2
                $this->getKey(),
586 2
                get_class($audit),
587 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

587
                $audit->/** @scrutinizer ignore-call */ 
588
                        getKey()
Loading history...
588 2
            ), array_keys($incompatibilities));
589
        }
590
591 10
        $key = $old ? 'old' : 'new';
592
593 10
        foreach ($modified as $attribute => $value) {
594 6
            if (array_key_exists($key, $value)) {
595 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

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