Passed
Push — master ( 5ef04a...5a4d36 )
by ANTERIO
27:24
created

Auditable::auditAttach()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
nc 5
nop 5
dl 0
loc 16
ccs 13
cts 14
cp 0.9286
crap 5.009
rs 9.6111
c 2
b 0
f 0
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 38
            'auditable'
79 38
        );
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
            if (
119 134
                is_array($value) ||
120 134
                (is_object($value) &&
121 134
                    !method_exists($value, '__toString') &&
122 134
                    !($value instanceof \UnitEnum))
0 ignored issues
show
Bug introduced by
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
123
            ) {
124 4
                $this->excludedAttributes[] = $attribute;
125
            }
126
        }
127
    }
128
129
    /**
130
     * @return array
131
     */
132 146
    public function getAuditExclude(): array
133
    {
134 146
        return $this->auditExclude ?? Config::get('audit.exclude', []);
135
    }
136
137
    /**
138
     * @return array
139
     */
140 136
    public function getAuditInclude(): array
141
    {
142 136
        return $this->auditInclude ?? [];
143
    }
144
145
    /**
146
     * Get the old/new attributes of a retrieved event.
147
     *
148
     * @return array
149
     */
150 6
    protected function getRetrievedEventAttributes(): array
151
    {
152
        // This is a read event with no attribute changes,
153
        // only metadata will be stored in the Audit
154
155 6
        return [
156 6
            [],
157 6
            [],
158 6
        ];
159
    }
160
161
    /**
162
     * Get the old/new attributes of a created event.
163
     *
164
     * @return array
165
     */
166 128
    protected function getCreatedEventAttributes(): array
167
    {
168 128
        $new = [];
169
170 128
        foreach ($this->attributes as $attribute => $value) {
171 120
            if ($this->isAttributeAuditable($attribute)) {
172 120
                $new[$attribute] = $value;
173
            }
174
        }
175
176 128
        return [
177 128
            [],
178 128
            $new,
179 128
        ];
180
    }
181
182 12
    protected function getCustomEventAttributes(): array
183
    {
184 12
        return [
185 12
            $this->auditCustomOld,
186 12
            $this->auditCustomNew
187 12
        ];
188
    }
189
190
    /**
191
     * Get the old/new attributes of an updated event.
192
     *
193
     * @return array
194
     */
195 18
    protected function getUpdatedEventAttributes(): array
196
    {
197 18
        $old = [];
198 18
        $new = [];
199
200 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

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

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

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

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

609
        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

609
        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...
610 2
            throw new AuditableTransitionException(sprintf(
611 2
                'Incompatibility between [%s:%s] and [%s:%s]',
612 2
                $this->getMorphClass(),
613 2
                $this->getKey(),
614 2
                get_class($audit),
615 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

615
                $audit->/** @scrutinizer ignore-call */ 
616
                        getKey()
Loading history...
616 2
            ), array_keys($incompatibilities));
617
        }
618
619 10
        $key = $old ? 'old' : 'new';
620
621 10
        foreach ($modified as $attribute => $value) {
622 6
            if (array_key_exists($key, $value)) {
623 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

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