Test Failed
Push — master ( 1a9cc9...cac184 )
by Morten
25:35
created

Auditable::preloadResolverData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 9
ccs 3
cts 3
cp 1
crap 2
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
     * @var array Preloaded data to be used by resolvers
61
     */
62
    public $preloadedResolverData = [];
63
64 218
    /**
65
     * Auditable boot logic.
66 218
     *
67 216
     * @return void
68
     */
69
    public static function bootAuditable()
70
    {
71
        if (static::isAuditingEnabled()) {
72
            static::observe(new AuditableObserver());
73
        }
74 40
    }
75
76 40
    /**
77 40
     * {@inheritdoc}
78 40
     */
79 40
    public function audits(): MorphMany
80
    {
81
        return $this->morphMany(
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

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

111
            if ($this->/** @scrutinizer ignore-call */ getCreatedAtColumn()) {
Loading history...
112 146
                $this->excludedAttributes[] = $this->getCreatedAtColumn();
113 138
            }
114
            if ($this->getUpdatedAtColumn()) {
0 ignored issues
show
Bug introduced by
It seems like getUpdatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

114
            if ($this->/** @scrutinizer ignore-call */ getUpdatedAtColumn()) {
Loading history...
115
                $this->excludedAttributes[] = $this->getUpdatedAtColumn();
116
            }
117
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
118 146
                $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

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

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

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

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

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

631
        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

631
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
632
            throw new AuditableTransitionException(sprintf(
633 12
                'Incompatibility between [%s:%s] and [%s:%s]',
634
                $this->getMorphClass(),
635
                $this->getKey(),
636
                get_class($audit),
637
                $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

637
                $audit->/** @scrutinizer ignore-call */ 
638
                        getKey()
Loading history...
638
            ), array_keys($incompatibilities));
639
        }
640
641
        $key = $old ? 'old' : 'new';
642
643
        foreach ($modified as $attribute => $value) {
644
            if (array_key_exists($key, $value)) {
645
                $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

645
                $this->/** @scrutinizer ignore-call */ 
646
                       setAttribute($attribute, $value[$key]);
Loading history...
646
            }
647
        }
648
649
        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...
650
    }
651
652
    /*
653 2
    |--------------------------------------------------------------------------
654
    | Pivot help methods
655 2
    |--------------------------------------------------------------------------
656
    |
657
    | Methods for auditing pivot actions
658 2
    |
659 2
    */
660 2
661 2
    /**
662 2
     * @param string $relationName
663 2
     * @param mixed $id
664 2
     * @param array $attributes
665 2
     * @param bool $touch
666 2
     * @param array $columns
667 2
     * @return void
668 2
     * @throws AuditingException
669
     */
670
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'])
671
    {
672
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'attach')) {
673
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method attach');
674
        }
675
676
        $old = $this->{$relationName}()->get($columns);
677
        $this->{$relationName}()->attach($id, $attributes, $touch);
678
        $new = $this->{$relationName}()->get($columns);
679
        $this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new);
680
    }
681
682
    /**
683
     * @param string $relationName
684
     * @param mixed $ids
685
     * @param bool $touch
686
     * @param array $columns
687
     * @return int
688
     * @throws AuditingException
689
     */
690
    public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'])
691
    {
692
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'detach')) {
693
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method detach');
694
        }
695
696
        $old = $this->{$relationName}()->get($columns);
697
        $results = $this->{$relationName}()->detach($ids, $touch);
698
        $new = $this->{$relationName}()->get($columns);
699
        $this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new);
700
701
        return empty($results) ? 0 : $results;
702
    }
703
704
    /**
705
     * @param $relationName
706 8
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
707
     * @param bool $detaching
708 8
     * @param array $columns
709
     * @return array
710
     * @throws AuditingException
711
     */
712 8
    public function auditSync($relationName, $ids, $detaching = true, $columns = ['*'])
713
    {
714 8
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
715 8
            throw new AuditingException('Relationship ' . $relationName . ' was not found or does not support method sync');
716 8
        }
717
718 8
        $old = $this->{$relationName}()->get($columns);
719
        $changes = $this->{$relationName}()->sync($ids, $detaching);
720 8
        if (collect($changes)->flatten()->isEmpty()) {
721 4
            $old = $new = collect([]);
722 4
        } else {
723
            $new = $this->{$relationName}()->get($columns);
724 4
        }
725 4
        $this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new);
726 4
727
        return $changes;
728
    }
729 8
730 8
    /**
731 8
     * @param string $relationName
732
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
733 8
     * @param array $columns
734
     * @return array
735
     * @throws AuditingException
736
     */
737
    public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'])
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
743
        return $this->auditSync($relationName, $ids, false, $columns);
744
    }
745
746
    /**
747
     * @param string $relationName
748
     * @param string $event
749
     * @param \Illuminate\Support\Collection $old
750
     * @param \Illuminate\Support\Collection $new
751
     * @return void
752
     */
753
    private function dispatchRelationAuditEvent($relationName, $event, $old, $new)
754
    {
755
        $this->auditCustomOld[$relationName] = $old->diff($new)->toArray();
756
        $this->auditCustomNew[$relationName] = $new->diff($old)->toArray();
757
758
        if (
759
            empty($this->auditCustomOld[$relationName]) &&
760
            empty($this->auditCustomNew[$relationName])
761
        ) {
762
            $this->auditCustomOld = $this->auditCustomNew = [];
763
        }
764
765
        $this->auditEvent = $event;
766
        $this->isCustomEvent = true;
767
        Event::dispatch(AuditCustom::class, [$this]);
768
        $this->isCustomEvent = false;
769
    }
770
}
771