Passed
Pull Request — master (#696)
by Morten
20:21
created

Auditable::getRetrievedEventAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 8
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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
22
    /**
23
     * Auditable attributes excluded from the Audit.
24
     *
25
     * @var array
26
     */
27
    protected $excludedAttributes = [];
28
29
    /**
30
     * Audit event name.
31
     *
32
     * @var string
33
     */
34
    protected $auditEvent;
35
36
    /**
37
     * Is auditing disabled?
38
     *
39
     * @var bool
40
     */
41
    public static $auditingDisabled = false;
42
43
    /**
44
     * Property may set custom event data to register
45
     * @var null|array
46
     */
47
    public $auditCustomOld = null;
48
49
    /**
50
     * Property may set custom event data to register
51
     * @var null|array
52
     */
53
    public $auditCustomNew = null;
54
55
    /**
56
     * If this is a custom event (as opposed to an eloquent event
57
     * @var bool
58
     */
59
    public $isCustomEvent = false;
60
61
    /**
62
     * Auditable boot logic.
63
     *
64
     * @return void
65
     */
66 204
    public static function bootAuditable()
67
    {
68 204
        if (!self::$auditingDisabled && static::isAuditingEnabled()) {
69 200
            static::observe(new AuditableObserver());
70
        }
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 38
    public function audits(): MorphMany
77
    {
78 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

78
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
79 38
            Config::get('audit.implementation', Models\Audit::class),
80
            'auditable'
81
        );
82
    }
83
84
    /**
85
     * Resolve the Auditable attributes to exclude from the Audit.
86
     *
87
     * @return void
88
     */
89 132
    protected function resolveAuditExclusions()
90
    {
91 132
        $this->excludedAttributes = $this->getAuditExclude();
92
93
        // When in strict mode, hidden and non visible attributes are excluded
94 132
        if ($this->getAuditStrict()) {
95
            // Hidden attributes
96 2
            $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
97
98
            // Non visible attributes
99 2
            if ($this->visible) {
100 2
                $invisible = array_diff(array_keys($this->attributes), $this->visible);
101
102 2
                $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
103
            }
104
        }
105
106
        // Exclude Timestamps
107 132
        if (!$this->getAuditTimestamps()) {
108 132
            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

108
            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

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

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

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

300
        if (!method_exists($this, /** @scrutinizer ignore-type */ $attributeGetter)) {
Loading history...
301 8
            throw new AuditingException(sprintf(
302
                'Unable to handle "%s" event, %s() method missing',
303 8
                $this->auditEvent,
304
                $attributeGetter
305
            ));
306
        }
307
308 132
        $this->resolveAuditExclusions();
309
310 132
        list($old, $new) = $this->$attributeGetter();
311
312 132
        if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
313 4
            foreach ($old as $attribute => $value) {
314 2
                $old[$attribute] = $this->modifyAttributeValue($attribute, $value);
315
            }
316
317 4
            foreach ($new as $attribute => $value) {
318 4
                $new[$attribute] = $this->modifyAttributeValue($attribute, $value);
319
            }
320
        }
321
322 130
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
323
324 130
        $tags = implode(',', $this->generateTags());
325
326 130
        $user = $this->resolveUser();
327
328 128
        return $this->transformAudit(array_merge([
329
            'old_values'           => $old,
330
            'new_values'           => $new,
331 128
            'event'                => $this->auditEvent,
332 128
            '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

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

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

601
        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

601
        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...
602 2
            throw new AuditableTransitionException(sprintf(
603
                'Incompatibility between [%s:%s] and [%s:%s]',
604 2
                $this->getMorphClass(),
605 2
                $this->getKey(),
606 2
                get_class($audit),
607 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

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

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