Test Failed
Pull Request — master (#684)
by Morten
07:18
created

Auditable::toAudit()   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.332

Importance

Changes 10
Bugs 2 Features 2
Metric Value
cc 10
eloc 28
c 10
b 2
f 2
nc 4
nop 0
dl 0
loc 46
ccs 20
cts 28
cp 0.7143
crap 12.332
rs 7.6666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Relations\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
    protected $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 42
    public static function bootAuditable()
65
    {
66 42
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
67 40
            static::observe(new AuditableObserver());
68
        }
69 42
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 12
    public function audits(): MorphMany
75
    {
76 12
        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 12
            Config::get('audit.implementation', Models\Audit::class),
78 12
            'auditable'
79
        );
80
    }
81
82
    /**
83
     * Resolve the Auditable attributes to exclude from the Audit.
84
     *
85
     * @return void
86
     */
87 34
    protected function resolveAuditExclusions()
88
    {
89 34
        $this->excludedAttributes = $this->getAuditExclude();
90
91
        // When in strict mode, hidden and non visible attributes are excluded
92 34
        if ($this->getAuditStrict()) {
93
            // Hidden attributes
94
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
95
96
            // Non visible attributes
97
            if ($this->visible) {
98
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
99
100
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
101
            }
102
        }
103
104
        // Exclude Timestamps
105 34
        if (!$this->getAuditTimestamps()) {
106 34
            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

106
            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

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

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

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

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

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

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

587
        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

587
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
588
            throw new AuditableTransitionException(sprintf(
589
                'Incompatibility between [%s:%s] and [%s:%s]',
590
                $this->getMorphClass(),
591
                $this->getKey(),
592
                get_class($audit),
593
                $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

593
                $audit->/** @scrutinizer ignore-call */ 
594
                        getKey()
Loading history...
594
            ), array_keys($incompatibilities));
595
        }
596
597
        $key = $old ? 'old' : 'new';
598
599
        foreach ($modified as $attribute => $value) {
600
            if (array_key_exists($key, $value)) {
601
                $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

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