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

81
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
82 20
            Config::get('audit.implementation', Models\Audit::class),
83 20
            'auditable'
84
        );
85
    }
86
87
    /**
88
     * Resolve the Auditable attributes to exclude from the Audit.
89
     *
90
     * @return void
91
     */
92 48
    protected function resolveAuditExclusions()
93
    {
94 48
        $this->excludedAttributes = $this->getAuditExclude();
95
96
        // When in strict mode, hidden and non visible attributes are excluded
97 48
        if ($this->getAuditStrict()) {
98
            // Hidden attributes
99
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
100
101
            // Non visible attributes
102
            if ($this->visible) {
103
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
104
105
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
106
            }
107
        }
108
109
        // Exclude Timestamps
110 48
        if (!$this->getAuditTimestamps()) {
111 48
            array_push($this->excludedAttributes, $this->getCreatedAtColumn(), $this->getUpdatedAtColumn());
0 ignored issues
show
Bug introduced by
It seems like getUpdatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

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

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

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

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

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

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

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

576
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
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

576
        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...
577
            throw new AuditableTransitionException(sprintf(
578
                'Incompatibility between [%s:%s] and [%s:%s]',
579
                $this->getMorphClass(),
580
                $this->getKey(),
581
                get_class($audit),
582
                $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

582
                $audit->/** @scrutinizer ignore-call */ 
583
                        getKey()
Loading history...
583
            ), array_keys($incompatibilities));
584
        }
585
586
        $key = $old ? 'old' : 'new';
587
588
        foreach ($modified as $attribute => $value) {
589
            if (array_key_exists($key, $value)) {
590
                $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

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