Passed
Push — master ( 332d13...2317a5 )
by ANTERIO
19:58 queued 12s
created

Auditable::audits()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 4
Bugs 3 Features 0
Metric Value
cc 1
eloc 3
c 4
b 3
f 0
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
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\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 214
    public static function bootAuditable()
65
    {
66 214
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
67 210
            static::observe(new AuditableObserver());
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 38
    public function audits(): MorphMany
75
    {
76 38
        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 38
            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 142
    protected function resolveAuditExclusions()
88
    {
89 142
        $this->excludedAttributes = $this->getAuditExclude();
90
91
        // When in strict mode, hidden and non visible attributes are excluded
92 142
        if ($this->getAuditStrict()) {
93
            // Hidden attributes
94 2
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
95
96
            // Non visible attributes
97 2
            if ($this->visible) {
98 2
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
99
100 2
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
101
            }
102
        }
103
104
        // Exclude Timestamps
105 142
        if (!$this->getAuditTimestamps()) {
106 142
            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 142
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
109 134
                $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 142
        $attributes = Arr::except($this->attributes, $this->excludedAttributes);
115
116 142
        foreach ($attributes as $attribute => $value) {
117
            // Apart from null, non scalar values will be excluded
118 134
            if (is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
119 4
                $this->excludedAttributes[] = $attribute;
120
            }
121
        }
122
    }
123
124
    /**
125
     * @return array
126
     */
127 146
    public function getAuditExclude(): array
128
    {
129 146
        return $this->auditExclude ?? Config::get('audit.exclude', []);
130
    }
131
132
    /**
133
     * @return array
134
     */
135 136
    public function getAuditInclude(): array
136
    {
137 136
        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 128
    protected function getCreatedEventAttributes(): array
162
    {
163 128
        $new = [];
164
165 128
        foreach ($this->attributes as $attribute => $value) {
166 120
            if ($this->isAttributeAuditable($attribute)) {
167 120
                $new[$attribute] = $value;
168
            }
169
        }
170
171
        return [
172 128
            [],
173
            $new,
174
        ];
175
    }
176
177 12
    protected function getCustomEventAttributes(): array
178
    {
179
        return [
180 12
            $this->auditCustomOld,
181 12
            $this->auditCustomNew
182
        ];
183
    }
184
185
    /**
186
     * Get the old/new attributes of an updated event.
187
     *
188
     * @return array
189
     */
190 18
    protected function getUpdatedEventAttributes(): array
191
    {
192 18
        $old = [];
193 18
        $new = [];
194
195 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

195
        foreach ($this->/** @scrutinizer ignore-call */ getDirty() as $attribute => $value) {
Loading history...
196 16
            if ($this->isAttributeAuditable($attribute)) {
197 14
                $old[$attribute] = Arr::get($this->original, $attribute);
198 14
                $new[$attribute] = Arr::get($this->attributes, $attribute);
199
            }
200
        }
201
202
        return [
203 18
            $old,
204
            $new,
205
        ];
206
    }
207
208
    /**
209
     * Get the old/new attributes of a deleted event.
210
     *
211
     * @return array
212
     */
213 8
    protected function getDeletedEventAttributes(): array
214
    {
215 8
        $old = [];
216
217 8
        foreach ($this->attributes as $attribute => $value) {
218 8
            if ($this->isAttributeAuditable($attribute)) {
219 8
                $old[$attribute] = $value;
220
            }
221
        }
222
223
        return [
224 8
            $old,
225
            [],
226
        ];
227
    }
228
229
    /**
230
     * Get the old/new attributes of a restored event.
231
     *
232
     * @return array
233
     */
234 4
    protected function getRestoredEventAttributes(): array
235
    {
236
        // A restored event is just a deleted event in reverse
237 4
        return array_reverse($this->getDeletedEventAttributes());
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 164
    public function readyForAuditing(): bool
244
    {
245 164
        if (static::$auditingDisabled) {
246 2
            return false;
247
        }
248
249 164
        if ($this->isCustomEvent) {
250 12
            return true;
251
        }
252
253 164
        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 4
    protected function modifyAttributeValue(string $attribute, $value)
267
    {
268 4
        $attributeModifiers = $this->getAttributeModifiers();
269
270 4
        if (!array_key_exists($attribute, $attributeModifiers)) {
271 2
            return $value;
272
        }
273
274 4
        $attributeModifier = $attributeModifiers[$attribute];
275
276 4
        if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
277 2
            return call_user_func([$attributeModifier, 'redact'], $value);
278
        }
279
280 4
        if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
281 2
            return call_user_func([$attributeModifier, 'encode'], $value);
282
        }
283
284 2
        throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290 152
    public function toAudit(): array
291
    {
292 152
        if (!$this->readyForAuditing()) {
293 2
            throw new AuditingException('A valid audit event has not been set');
294
        }
295
296 150
        $attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
297
298 150
        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 8
            throw new AuditingException(sprintf(
300
                'Unable to handle "%s" event, %s() method missing',
301 8
                $this->auditEvent,
302
                $attributeGetter
303
            ));
304
        }
305
306 142
        $this->resolveAuditExclusions();
307
308 142
        list($old, $new) = $this->$attributeGetter();
309
310 142
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
311 4
            foreach ($old as $attribute => $value) {
312 2
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
313
            }
314
315 4
            foreach ($new as $attribute => $value) {
316 4
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
317
            }
318
        }
319
320 140
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
321
322 140
        $tags = implode(',', $this->generateTags());
323
324 140
        $user = $this->resolveUser();
325
326 138
        return $this->transformAudit(array_merge([
327
            'old_values'           => $old,
328
            'new_values'           => $new,
329 138
            'event'                => $this->auditEvent,
330 138
            '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 138
            '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 138
            $morphPrefix . '_id'   => $user ? $user->getAuthIdentifier() : null,
333 138
            $morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
334 138
            'tags'                 => empty($tags) ? null : $tags,
335 138
        ], $this->runResolvers()));
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 130
    public function transformAudit(array $data): array
342
    {
343 130
        return $data;
344
    }
345
346
    /**
347
     * Resolve the User.
348
     *
349
     * @return mixed|null
350
     * @throws AuditingException
351
     *
352
     */
353 140
    protected function resolveUser()
354
    {
355 140
        $userResolver = Config::get('audit.user.resolver');
356
357 140
        if (is_null($userResolver) && Config::has('audit.resolver') && !Config::has('audit.user.resolver')) {
358
            trigger_error('The config file audit.php is not updated to the new version 13.0. Please see https://www.laravel-auditing.com/docs/13.0/upgrading',
359
                E_USER_DEPRECATED);
360
            $userResolver = Config::get('audit.resolver.user');
361
        }
362
363 140
        if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
364 138
            return call_user_func([$userResolver, 'resolve']);
365
        }
366
367 2
        throw new AuditingException('Invalid UserResolver implementation');
368
    }
369
370 138
    protected function runResolvers(): array
371
    {
372 138
        $resolved = [];
373 138
        if (Config::has('audit.resolver')) {
374
            trigger_error('The config file audit.php is not updated to the new version 13.0. Please see https://www.laravel-auditing.com/docs/13.0/upgrading',
375
                E_USER_DEPRECATED);
376
            return [];
377
        }
378
379 138
        foreach (Config::get('audit.resolvers', []) as $name => $implementation) {
380 138
            if (empty($implementation)) {
381 2
                continue;
382
            }
383
384 138
            if (!is_subclass_of($implementation, Resolver::class)) {
385 6
                throw new AuditingException('Invalid Resolver implementation for: ' . $name);
386
            }
387 136
            $resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
388
        }
389 132
        return $resolved;
390
    }
391
392
    /**
393
     * Determine if an attribute is eligible for auditing.
394
     *
395
     * @param string $attribute
396
     *
397
     * @return bool
398
     */
399 132
    protected function isAttributeAuditable(string $attribute): bool
400
    {
401
        // The attribute should not be audited
402 132
        if (in_array($attribute, $this->excludedAttributes, true)) {
403 126
            return false;
404
        }
405
406
        // The attribute is auditable when explicitly
407
        // listed or when the include array is empty
408 132
        $include = $this->getAuditInclude();
409
410 132
        return empty($include) || in_array($attribute, $include, true);
411
    }
412
413
    /**
414
     * Determine whether an event is auditable.
415
     *
416
     * @param string $event
417
     *
418
     * @return bool
419
     */
420 166
    protected function isEventAuditable($event): bool
421
    {
422 166
        return is_string($this->resolveAttributeGetter($event));
423
    }
424
425
    /**
426
     * Attribute getter method resolver.
427
     *
428
     * @param string $event
429
     *
430
     * @return string|null
431
     */
432 166
    protected function resolveAttributeGetter($event)
433
    {
434 166
        if (empty($event)) {
435 62
            return;
436
        }
437
438 166
        if ($this->isCustomEvent) {
439 12
            return 'getCustomEventAttributes';
440
        }
441
442 166
        foreach ($this->getAuditEvents() as $key => $value) {
443 166
            $auditableEvent = is_int($key) ? $value : $key;
444
445 166
            $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
446
447 166
            if (preg_match($auditableEventRegex, $event)) {
448 162
                return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
449
            }
450
        }
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     */
456 166
    public function setAuditEvent(string $event): Contracts\Auditable
457
    {
458 166
        $this->auditEvent = $this->isEventAuditable($event) ? $event : null;
459
460 166
        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...
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466 128
    public function getAuditEvent()
467
    {
468 128
        return $this->auditEvent;
469
    }
470
471
    /**
472
     * {@inheritdoc}
473
     */
474 172
    public function getAuditEvents(): array
475
    {
476 172
        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...
477
                'created',
478
                'updated',
479
                'deleted',
480
                'restored',
481
            ]);
482
    }
483
484
    /**
485
     * Disable Auditing.
486
     *
487
     * @return void
488
     */
489 2
    public static function disableAuditing()
490
    {
491 2
        static::$auditingDisabled = true;
492
    }
493
494
    /**
495
     * Enable Auditing.
496
     *
497
     * @return void
498
     */
499 2
    public static function enableAuditing()
500
    {
501 2
        static::$auditingDisabled = false;
502
    }
503
504
    /**
505
     * Determine whether auditing is enabled.
506
     *
507
     * @return bool
508
     */
509 218
    public static function isAuditingEnabled(): bool
510
    {
511 218
        if (App::runningInConsole()) {
512 214
            return Config::get('audit.enabled', true) && Config::get('audit.console', false);
513
        }
514
515 4
        return Config::get('audit.enabled', true);
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521 148
    public function getAuditStrict(): bool
522
    {
523 148
        return $this->auditStrict ?? Config::get('audit.strict', false);
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     */
529 148
    public function getAuditTimestamps(): bool
530
    {
531 148
        return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
532
    }
533
534
    /**
535
     * {@inheritdoc}
536
     */
537 136
    public function getAuditDriver()
538
    {
539 136
        return $this->auditDriver ?? Config::get('audit.driver', 'database');
540
    }
541
542
    /**
543
     * {@inheritdoc}
544
     */
545 130
    public function getAuditThreshold(): int
546
    {
547 130
        return $this->auditThreshold ?? Config::get('audit.threshold', 0);
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     */
553 142
    public function getAttributeModifiers(): array
554
    {
555 142
        return $this->attributeModifiers ?? [];
556
    }
557
558
    /**
559
     * {@inheritdoc}
560
     */
561 142
    public function generateTags(): array
562
    {
563 142
        return [];
564
    }
565
566
    /**
567
     * {@inheritdoc}
568
     */
569 22
    public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
570
    {
571
        // The Audit must be for an Auditable model of this type
572 22
        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...
573 4
            throw new AuditableTransitionException(sprintf(
574
                'Expected Auditable type %s, got %s instead',
575 4
                $this->getMorphClass(),
576 4
                $audit->auditable_type
577
            ));
578
        }
579
580
        // The Audit must be for this specific Auditable model
581 18
        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...
582 4
            throw new AuditableTransitionException(sprintf(
583
                'Expected Auditable id %s, got %s instead',
584 4
                $this->getKey(),
585 4
                $audit->auditable_id
586
            ));
587
        }
588
589
        // Redacted data should not be used when transitioning states
590 14
        foreach ($this->getAttributeModifiers() as $attribute => $modifier) {
591 2
            if (is_subclass_of($modifier, AttributeRedactor::class)) {
592 2
                throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set');
593
            }
594
        }
595
596
        // The attribute compatibility between the Audit and the Auditable model must be met
597 12
        $modified = $audit->getModified();
598
599 12
        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

599
        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

599
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
600 2
            throw new AuditableTransitionException(sprintf(
601
                'Incompatibility between [%s:%s] and [%s:%s]',
602 2
                $this->getMorphClass(),
603 2
                $this->getKey(),
604 2
                get_class($audit),
605 2
                $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

605
                $audit->/** @scrutinizer ignore-call */ 
606
                        getKey()
Loading history...
606 2
            ), array_keys($incompatibilities));
607
        }
608
609 10
        $key = $old ? 'old' : 'new';
610
611 10
        foreach ($modified as $attribute => $value) {
612 6
            if (array_key_exists($key, $value)) {
613 6
                $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

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