Test Failed
Pull Request — master (#684)
by Morten
14:54
created

Auditable::getAuditEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Relations\MorphMany;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Facades\App;
9
use Illuminate\Support\Facades\Config;
10
use Illuminate\Support\Facades\Event;
11
use OwenIt\Auditing\Contracts\AttributeEncoder;
12
use OwenIt\Auditing\Contracts\AttributeRedactor;
13
use OwenIt\Auditing\Contracts\Resolver;
14
use OwenIt\Auditing\Events\AuditCustom;
15
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
16
use OwenIt\Auditing\Exceptions\AuditingException;
17
18
trait Auditable
19
{
20
    /**
21
     * Auditable attributes excluded from the Audit.
22
     *
23
     * @var array
24
     */
25
    protected $excludedAttributes = [];
26
27
    /**
28
     * Audit event name.
29
     *
30
     * @var string
31
     */
32
    protected $auditEvent;
33
34
    /**
35
     * Is auditing disabled?
36
     *
37
     * @var bool
38
     */
39
    public static $auditingDisabled = false;
40
41
    /**
42
     * Property may set custom event data to register
43
     * @var null|array
44
     */
45
    public $auditCustomOld = null;
46
47
    /**
48
     * Property may set custom event data to register
49
     * @var null|array
50
     */
51
    public $auditCustomNew = null;
52
53
    /**
54
     * If this is a custom event (as opposed to an eloquent event
55
     * @var bool
56
     */
57
    public $isCustomEvent = false;
58
59
    /**
60
     * Auditable boot logic.
61
     *
62
     * @return void
63
     */
64 40
    public static function bootAuditable()
65
    {
66 40
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
67 38
            static::observe(new AuditableObserver());
68
        }
69 40
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 10
    public function audits(): MorphMany
75
    {
76 10
        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 10
            Config::get('audit.implementation', Models\Audit::class),
78 10
            'auditable'
79
        );
80
    }
81
82
    /**
83
     * Resolve the Auditable attributes to exclude from the Audit.
84
     *
85
     * @return void
86
     */
87 32
    protected function resolveAuditExclusions()
88
    {
89 32
        $this->excludedAttributes = $this->getAuditExclude();
90
91
        // When in strict mode, hidden and non visible attributes are excluded
92 32
        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 32
        if (!$this->getAuditTimestamps()) {
106 32
            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 32
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
109 24
                $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 32
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
115
116 32
        foreach ($attributes as $attribute => $value) {
117
            // Apart from null, non scalar values will be excluded
118 32
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
119
                $this->excludedAttributes[] = $attribute;
120
            }
121
        }
122 32
    }
123
124
    /**
125
     * Get the old/new attributes of a retrieved event.
126
     *
127
     * @return array
128
     */
129 4
    protected function getRetrievedEventAttributes(): array
130
    {
131
        // This is a read event with no attribute changes,
132
        // only metadata will be stored in the Audit
133
134
        return [
135 4
            [],
136
            [],
137
        ];
138
    }
139
140
    /**
141
     * Get the old/new attributes of a created event.
142
     *
143
     * @return array
144
     */
145 20
    protected function getCreatedEventAttributes(): array
146
    {
147 20
        $new = [];
148
149 20
        foreach ($this->attributes as $attribute => $value) {
150 20
            if ($this->isAttributeAuditable($attribute)) {
151 20
                $new[$attribute] = $value;
152
            }
153
        }
154
155
        return [
156 20
            [],
157 20
            $new,
158
        ];
159
    }
160
161
    protected function getCustomEventAttributes(): array
162
    {
163
        return [
164
            $this->auditCustomOld,
165
            $this->auditCustomNew
166
        ];
167
    }
168
169
    /**
170
     * Get the old/new attributes of an updated event.
171
     *
172
     * @return array
173
     */
174 8
    protected function getUpdatedEventAttributes(): array
175
    {
176 8
        $old = [];
177 8
        $new = [];
178
179 8
        foreach ($this->getDirty() as $attribute => $value) {
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

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

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

282
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
283
            throw new AuditingException(sprintf(
284
                'Unable to handle "%s" event, %s() method missing',
285
                $this->auditEvent,
286
                $attributeGetter
287
            ));
288
        }
289
290 32
        $this->resolveAuditExclusions();
291
292 32
        list($old, $new) = $this->$attributeGetter();
293
294 32
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
295
            foreach ($old as $attribute => $value) {
296
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
297
            }
298
299
            foreach ($new as $attribute => $value) {
300
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
301
            }
302
        }
303
304 32
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
305
306 32
        $tags = implode(',', $this->generateTags());
307
308 32
        $user = $this->resolveUser();
309
310 32
        return $this->transformAudit(array_merge([
311 32
            'old_values'           => $old,
312 32
            'new_values'           => $new,
313 32
            'event'                => $this->auditEvent,
314 32
            'auditable_id'         => $this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

314
            'auditable_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
315 32
            'auditable_type'       => $this->getMorphClass(),
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

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

592
        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

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

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

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