Test Failed
Pull Request — master (#710)
by
unknown
18:35
created

Auditable::toAudit()   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 12.1952

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 18
cts 25
cp 0.72
crap 12.1952
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
    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 70
    public static function bootAuditable()
65
    {
66 70
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
67 68
            static::observe(new AuditableObserver());
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 32
    public function audits(): MorphMany
75
    {
76 32
        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 32
            Config::get('audit.implementation', Models\Audit::class),
78
            'auditable'
79
        );
80
    }
81
82
    /**
83
     * Resolve the Auditable attributes to exclude from the Audit.
84
     *
85
     * @return void
86
     */
87 62
    protected function resolveAuditExclusions()
88
    {
89 62
        $this->excludedAttributes = $this->getAuditExclude();
90
91
        // When in strict mode, hidden and non visible attributes are excluded
92 62
        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 62
        if (!$this->getAuditTimestamps()) {
106 62
            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 62
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
109 54
                $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 62
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
115
116 62
        foreach ($attributes as $attribute => $value) {
117
            // Apart from null, non scalar values will be excluded
118 62
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
119
                $this->excludedAttributes[] = $attribute;
120
            }
121
        }
122
    }
123
124
    /**
125
     * @return array
126
     */
127 62
    public function getAuditExclude(): array
128
    {
129 62
        return $this->auditExclude ?? Config::get('audit.exclude', []);
130
    }
131
132
    /**
133
     * @return array
134
     */
135 60
    public function getAuditInclude(): array
136
    {
137 60
        return $this->auditInclude ?? [];
138
    }
139
140
    /**
141
     * Get the old/new attributes of a retrieved event.
142
     *
143
     * @return array
144
     */
145 6
    protected function getRetrievedEventAttributes(): array
146
    {
147
        // This is a read event with no attribute changes,
148
        // only metadata will be stored in the Audit
149
150
        return [
151 6
            [],
152
            [],
153
        ];
154
    }
155
156
    /**
157
     * Get the old/new attributes of a created event.
158
     *
159
     * @return array
160
     */
161 50
    protected function getCreatedEventAttributes(): array
162
    {
163 50
        $new = [];
164
165 50
        foreach ($this->attributes as $attribute => $value) {
166 50
            if ($this->isAttributeAuditable($attribute)) {
167 50
                $new[$attribute] = $value;
168
            }
169
        }
170
171
        return [
172 50
            [],
173
            $new,
174
        ];
175
    }
176
177 4
    protected function getCustomEventAttributes(): array
178
    {
179
        return [
180 4
            $this->auditCustomOld,
181 4
            $this->auditCustomNew
182
        ];
183
    }
184
185
    /**
186
     * Get the old/new attributes of an updated event.
187
     *
188
     * @return array
189
     */
190 12
    protected function getUpdatedEventAttributes(): array
191
    {
192 12
        $old = [];
193 12
        $new = [];
194
195 12
        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

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

298
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
299
            throw new AuditingException(sprintf(
300
                'Unable to handle "%s" event, %s() method missing',
301
                $this->auditEvent,
302
                $attributeGetter
303
            ));
304
        }
305
306 62
        $this->resolveAuditExclusions();
307
308 62
        list($old, $new) = $this->$attributeGetter();
309
310 62
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
311
            foreach ($old as $attribute => $value) {
312
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
313
            }
314
315
            foreach ($new as $attribute => $value) {
316
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
317
            }
318
        }
319
320 62
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
321
322 62
        $tags = implode(',', $this->generateTags());
323
324 62
        $user = $this->resolveUser();
325
326 62
        return $this->transformAudit(array_merge([
327
            'old_values'           => $old,
328
            'new_values'           => $new,
329 62
            'event'                => $this->auditEvent,
330 62
            '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

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

331
            'auditable_type'       => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
332 62
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
333 62
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
334 62
            'tags'                 => empty($tags) ? null : $tags,
335 62
        ], $this->runResolvers()));
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 62
    public function transformAudit(array $data): array
342
    {
343 62
        return $data;
344
    }
345
346
    /**
347
     * Resolve the User.
348
     *
349
     * @return mixed|null
350
     * @throws AuditingException
351
     *
352
     */
353 62
    protected function resolveUser()
354
    {
355 62
        $userResolver = Config::get('audit.user.resolver');
356
357 62
        if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
358 62
            return call_user_func([$userResolver, 'resolve']);
359
        }
360
361
        throw new AuditingException('Invalid UserResolver implementation');
362
    }
363
364 62
    protected function runResolvers(): array
365
    {
366 62
        $resolved = [];
367 62
        foreach (Config::get('audit.resolvers', []) as $name => $implementation) {
368 62
            if (empty($implementation)) {
369 2
                continue;
370
            }
371
372 62
            if (!is_subclass_of($implementation, Resolver::class)) {
373
                throw new AuditingException('Invalid Resolver implementation for: ' . $name);
374
            }
375 62
            $resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
376
        }
377 62
        return $resolved;
378
    }
379
380
    /**
381
     * Determine if an attribute is eligible for auditing.
382
     *
383
     * @param string $attribute
384
     *
385
     * @return bool
386
     */
387 60
    protected function isAttributeAuditable(string $attribute): bool
388
    {
389
        // The attribute should not be audited
390 60
        if (in_array($attribute, $this->excludedAttributes, true)) {
391 57
            return false;
392
        }
393
394
        // The attribute is auditable when explicitly
395
        // listed or when the include array is empty
396 60
        $include = $this->getAuditInclude();
397
398 60
        return empty($include) || in_array($attribute, $include, true);
399
    }
400
401
    /**
402
     * Determine whether an event is auditable.
403
     *
404
     * @param string $event
405
     *
406
     * @return bool
407
     */
408 68
    protected function isEventAuditable($event): bool
409
    {
410 68
        return is_string($this->resolveAttributeGetter($event));
411
    }
412
413
    /**
414
     * Attribute getter method resolver.
415
     *
416
     * @param string $event
417
     *
418
     * @return string|null
419
     */
420 68
    protected function resolveAttributeGetter($event)
421
    {
422 68
        if (empty($event)) {
423 36
            return;
424
        }
425
426 68
        if ($this->isCustomEvent) {
427 4
            return 'getCustomEventAttributes';
428
        }
429
430 68
        foreach ($this->getAuditEvents() as $key => $value) {
431 68
            $auditableEvent = is_int($key) ? $value : $key;
432
433 68
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
434
435 68
            if (preg_match($auditableEventRegex, $event)) {
436 68
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
437
            }
438
        }
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444 68
    public function setAuditEvent(string $event): Contracts\Auditable
445
    {
446 68
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
447
448 68
        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...
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454 4
    public function getAuditEvent()
455
    {
456 4
        return $this->auditEvent;
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462 68
    public function getAuditEvents(): array
463
    {
464 68
        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...
465
                'created',
466
                'updated',
467
                'deleted',
468
                'restored',
469
            ]);
470
    }
471
472
    /**
473
     * Disable Auditing.
474
     *
475
     * @return void
476
     */
477 2
    public static function disableAuditing()
478
    {
479 2
        static::$auditingDisabled = true;
480
    }
481
482
    /**
483
     * Enable Auditing.
484
     *
485
     * @return void
486
     */
487 2
    public static function enableAuditing()
488
    {
489 2
        static::$auditingDisabled = false;
490
    }
491
492
    /**
493
     * Determine whether auditing is enabled.
494
     *
495
     * @return bool
496
     */
497 70
    public static function isAuditingEnabled(): bool
498
    {
499 70
        if (App::runningInConsole()) {
500 68
            return Config::get('audit.enabled', true) && Config::get('audit.console', false);
501
        }
502
503 2
        return Config::get('audit.enabled', true);
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     */
509 62
    public function getAuditStrict(): bool
510
    {
511 62
        return $this->auditStrict ?? Config::get('audit.strict', false);
512
    }
513
514
    /**
515
     * {@inheritdoc}
516
     */
517 62
    public function getAuditTimestamps(): bool
518
    {
519 62
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 68
    public function getAuditDriver()
526
    {
527 68
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533 62
    public function getAuditThreshold(): int
534
    {
535 62
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 62
    public function getAttributeModifiers(): array
542
    {
543 62
        return $this->attributeModifiers ?? [];
544
    }
545
546
    /**
547
     * {@inheritdoc}
548
     */
549 62
    public function generateTags(): array
550
    {
551 62
        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 2
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['name'])
626
    {
627 2
        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 2
        $this->auditEvent = 'attach';
631 2
        $this->isCustomEvent = true;
632 2
        $this->auditCustomOld = [
633 2
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
634
        ];
635 2
        $this->{$relationName}()->attach($id, $attributes, $touch);
636 2
        $this->auditCustomNew = [
637 2
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
638
        ];
639 2
        Event::dispatch(AuditCustom::class, [$this]);
640 2
        $this->isCustomEvent = false;
641
    }
642
643
    /**
644
     * @param string $relationName
645
     * @param mixed $ids
646
     * @param bool $touch
647
     * @return int
648
     * @throws AuditingException
649
     */
650
    public function auditDetach(string $relationName, $ids = null, $touch = true)
651
    {
652
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'detach')) {
653
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method detach');
654
        }
655
656
        $this->auditEvent = 'detach';
657
        $this->isCustomEvent = true;
658
        $this->auditCustomOld = [
659
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
660
        ];
661
        $results = $this->{$relationName}()->detach($ids, $touch);
662
        $this->auditCustomNew = [
663
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
664
        ];
665
        Event::dispatch(AuditCustom::class, [$this]);
666
        $this->isCustomEvent = false;
667
        return empty($results) ? 0 : $results;
668
    }
669
670
    /**
671
     * @param $relationName
672
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
673
     * @param bool $detaching
674
     * @return array
675
     * @throws AuditingException
676
     */
677
    public function auditSync($relationName, $ids, $detaching = true)
678
    {
679
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
680
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method sync');
681
        }
682
683
        $this->auditEvent = 'sync';
684
        $this->isCustomEvent = true;
685
        $this->auditCustomOld = [
686
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
687
        ];
688
        $changes = $this->{$relationName}()->sync($ids, $detaching);
689
        $this->auditCustomNew = [
690
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
691
        ];
692
        Event::dispatch(AuditCustom::class, [$this]);
693
        $this->isCustomEvent = false;
694
        return $changes;
695
    }
696
697
    /**
698
     * @param string $relationName
699
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
700
     * @return array
701
     * @throws AuditingException
702
     */
703
    public function auditSyncWithoutDetaching(string $relationName, $ids)
704
    {
705
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'syncWithoutDetaching')) {
706
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method syncWithoutDetaching');
707
        }
708
        return $this->auditSync($relationName, $ids, false);
709
    }
710
}
711