Test Failed
Pull Request — master (#684)
by Morten
06:16
created

Auditable::getAuditThreshold()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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