Test Failed
Pull Request — master (#707)
by
unknown
09:28
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 56
    public static function bootAuditable()
65
    {
66 56
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
67 54
            static::observe(new AuditableObserver());
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 16
    public function audits(): MorphMany
75
    {
76 16
        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 16
            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 48
    protected function resolveAuditExclusions()
88
    {
89 48
        $this->excludedAttributes = $this->getAuditExclude();
90
91
        // When in strict mode, hidden and non visible attributes are excluded
92 48
        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 48
        if (!$this->getAuditTimestamps()) {
106 48
            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 48
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
109 40
                $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 48
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
115
116 48
        foreach ($attributes as $attribute => $value) {
117
            // Apart from null, non scalar values will be excluded
118 48
            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 48
    public function getAuditExclude(): array
128
    {
129 48
        return $this->auditExclude ?? Config::get('audit.exclude', []);
130
    }
131
132
    /**
133
     * @return array
134
     */
135 46
    public function getAuditInclude(): array
136
    {
137 46
        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 36
    protected function getCreatedEventAttributes(): array
162
    {
163 36
        $new = [];
164
165 36
        foreach ($this->attributes as $attribute => $value) {
166 36
            if ($this->isAttributeAuditable($attribute)) {
167 36
                $new[$attribute] = $value;
168
            }
169
        }
170
171
        return [
172 36
            [],
173
            $new,
174
        ];
175
    }
176
177 6
    protected function getCustomEventAttributes(): array
178
    {
179
        return [
180 6
            $this->auditCustomOld,
181 6
            $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 54
    public function readyForAuditing(): bool
244
    {
245 54
        if (static::$auditingDisabled) {
246 2
            return false;
247
        }
248
249 54
        if ($this->isCustomEvent) {
250 6
            return true;
251
        }
252
253 54
        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 48
    public function toAudit(): array
291
    {
292 48
        if (!$this->readyForAuditing()) {
293
            throw new AuditingException('A valid audit event has not been set');
294
        }
295
296 48
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
297
298 48
        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 48
        $this->resolveAuditExclusions();
307
308 48
        list($old, $new) = $this->$attributeGetter();
309
310 48
        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 48
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
321
322 48
        $tags = implode(',', $this->generateTags());
323
324 48
        $user = $this->resolveUser();
325
326 48
        return $this->transformAudit(array_merge([
327
            'old_values'           => $old,
328
            'new_values'           => $new,
329 48
            'event'                => $this->auditEvent,
330 48
            '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 48
            '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 48
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
333 48
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
334 48
            'tags'                 => empty($tags) ? null : $tags,
335 48
        ], $this->runResolvers()));
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 48
    public function transformAudit(array $data): array
342
    {
343 48
        return $data;
344
    }
345
346
    /**
347
     * Resolve the User.
348
     *
349
     * @return mixed|null
350
     * @throws AuditingException
351
     *
352
     */
353 48
    protected function resolveUser()
354
    {
355 48
        $userResolver = Config::get('audit.user.resolver');
356
357 48
        if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
358 48
            return call_user_func([$userResolver, 'resolve']);
359
        }
360
361
        throw new AuditingException('Invalid UserResolver implementation');
362
    }
363
364 48
    protected function runResolvers(): array
365
    {
366 48
        $resolved = [];
367 48
        foreach (Config::get('audit.resolvers', []) as $name => $implementation) {
368 48
            if (empty($implementation)) {
369 2
                continue;
370
            }
371
372 48
            if (!is_subclass_of($implementation, Resolver::class)) {
373
                throw new AuditingException('Invalid Resolver implementation for: ' . $name);
374
            }
375 48
            $resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
376
        }
377 48
        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 46
    protected function isAttributeAuditable(string $attribute): bool
388
    {
389
        // The attribute should not be audited
390 46
        if (in_array($attribute, $this->excludedAttributes, true)) {
391 44
            return false;
392
        }
393
394
        // The attribute is auditable when explicitly
395
        // listed or when the include array is empty
396 46
        $include = $this->getAuditInclude();
397
398 46
        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 54
    protected function isEventAuditable($event): bool
409
    {
410 54
        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 54
    protected function resolveAttributeGetter($event)
421
    {
422 54
        if (empty($event)) {
423 22
            return;
424
        }
425
426 54
        if ($this->isCustomEvent) {
427 6
            return 'getCustomEventAttributes';
428
        }
429
430 54
        foreach ($this->getAuditEvents() as $key => $value) {
431 54
            $auditableEvent = is_int($key) ? $value : $key;
432
433 54
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
434
435 54
            if (preg_match($auditableEventRegex, $event)) {
436 54
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
437
            }
438
        }
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444 54
    public function setAuditEvent(string $event): Contracts\Auditable
445
    {
446 54
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
447
448 54
        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 48
    public function getAuditEvent()
455
    {
456 48
        return $this->auditEvent;
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462 54
    public function getAuditEvents(): array
463
    {
464 54
        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 56
    public static function isAuditingEnabled(): bool
498
    {
499 56
        if (App::runningInConsole()) {
500 54
            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 48
    public function getAuditStrict(): bool
510
    {
511 48
        return $this->auditStrict ?? Config::get('audit.strict', false);
512
    }
513
514
    /**
515
     * {@inheritdoc}
516
     */
517 48
    public function getAuditTimestamps(): bool
518
    {
519 48
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 54
    public function getAuditDriver()
526
    {
527 54
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533 48
    public function getAuditThreshold(): int
534
    {
535 48
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 48
    public function getAttributeModifiers(): array
542
    {
543 48
        return $this->attributeModifiers ?? [];
544
    }
545
546
    /**
547
     * {@inheritdoc}
548
     */
549 48
    public function generateTags(): array
550
    {
551 48
        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
     * @param bool $skipUnchanged
675
     * @return array
676
     * @throws AuditingException
677
     */
678 4
    public function auditSync($relationName, $ids, $detaching = true)
679
    {
680 4
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
681
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method sync');
682
        }
683
684 4
        $this->auditEvent = 'sync';
685
686 4
        $this->auditCustomOld = [
687 4
            $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
688
        ];
689
690 4
        $changes = $this->{$relationName}()->sync($ids, $detaching);
691
692 4
        if (collect($changes)->flatten()->isEmpty()) {
693 2
            $this->auditCustomOld = [];
694 2
            $this->auditCustomNew = [];
695
        } else {
696 2
            $this->auditCustomNew = [
697 2
                $relationName => $this->{$relationName}()->get()->isEmpty() ? [] : $this->{$relationName}()->get()->toArray()
698
            ];
699
        }
700
701 4
        $this->isCustomEvent = true;
702 4
        Event::dispatch(AuditCustom::class, [$this]);
703 4
        $this->isCustomEvent = false;
704
705 4
        return $changes;
706
    }
707
708
    /**
709
     * @param string $relationName
710
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
711
     * @param bool $skipUnchanged
712
     * @return array
713
     * @throws AuditingException
714
     */
715
    public function auditSyncWithoutDetaching(string $relationName, $ids)
716
    {
717
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'syncWithoutDetaching')) {
718
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method syncWithoutDetaching');
719
        }
720
        return $this->auditSync($relationName, $ids, false);
721
    }
722
}
723