Issues (43)

src/Auditable.php (19 issues)

1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7
use Illuminate\Database\Eloquent\Relations\MorphMany;
8
use Illuminate\Database\Eloquent\SoftDeletes;
9
use Illuminate\Support\Arr;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Facades\App;
12
use Illuminate\Support\Facades\Config;
13
use Illuminate\Support\Facades\Event;
14
use OwenIt\Auditing\Contracts\AttributeEncoder;
15
use OwenIt\Auditing\Contracts\AttributeRedactor;
16
use OwenIt\Auditing\Contracts\Resolver;
17
use OwenIt\Auditing\Events\AuditCustom;
18
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
19
use OwenIt\Auditing\Exceptions\AuditingException;
20
21
// @phpstan-ignore trait.unused
22
trait Auditable
23
{
24
    /**
25
     * Auditable attributes excluded from the Audit.
26
     *
27
     * @var array
28
     */
29
    protected $excludedAttributes = [];
30
31
    /**
32
     * Audit event name.
33
     *
34
     * @var string
35
     */
36
    public $auditEvent;
37
38
    /**
39
     * Is auditing disabled?
40
     *
41
     * @var bool
42
     */
43
    public static $auditingDisabled = false;
44
45
    /**
46
     * Property may set custom event data to register
47
     * @var null|array
48
     */
49
    public $auditCustomOld = null;
50
51
    /**
52
     * Property may set custom event data to register
53
     * @var null|array
54
     */
55
    public $auditCustomNew = null;
56
57
    /**
58
     * If this is a custom event (as opposed to an eloquent event
59
     * @var bool
60
     */
61
    public $isCustomEvent = false;
62
63
    /**
64
     * @var array Preloaded data to be used by resolvers
65
     */
66
    public $preloadedResolverData = [];
67
68
    /**
69
     * Auditable boot logic.
70
     *
71
     * @return void
72 17
     */
73
    public static function bootAuditable()
74 17
    {
75 15
        if (static::isAuditingEnabled()) {
76
            static::observe(new AuditableObserver());
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82 15
     */
83
    public function audits(): MorphMany
84 15
    {
85 15
        return $this->morphMany(
0 ignored issues
show
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

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

115
            if ($this->/** @scrutinizer ignore-call */ getCreatedAtColumn()) {
Loading history...
116
                $this->excludedAttributes[] = $this->getCreatedAtColumn();
117 15
            }
118 15
            if ($this->getUpdatedAtColumn()) {
0 ignored issues
show
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

118
            if ($this->/** @scrutinizer ignore-call */ getUpdatedAtColumn()) {
Loading history...
119
                $this->excludedAttributes[] = $this->getUpdatedAtColumn();
120 15
            }
121 7
            if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
122
                $this->excludedAttributes[] = $this->getDeletedAtColumn();
0 ignored issues
show
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

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

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

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

348
            'auditable_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
349 15
            'auditable_type'       => $this->getMorphClass(),
0 ignored issues
show
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

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

673
        if ($incompatibilities = array_diff_key(/** @scrutinizer ignore-type */ $modified, $this->getAttributes())) {
Loading history...
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

673
        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...
674
            throw new AuditableTransitionException(sprintf(
675
                'Incompatibility between [%s:%s] and [%s:%s]',
676
                $this->getMorphClass(),
677
                $this->getKey(),
678
                get_class($audit),
679
                $audit->getKey()
0 ignored issues
show
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

679
                $audit->/** @scrutinizer ignore-call */ 
680
                        getKey()
Loading history...
680
            ), array_keys($incompatibilities));
681
        }
682
683
        $key = $old ? 'old' : 'new';
684
685
        foreach ($modified as $attribute => $value) {
686
            if (array_key_exists($key, $value)) {
687
                $this->setAttribute($attribute, $value[$key]);
0 ignored issues
show
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

687
                $this->/** @scrutinizer ignore-call */ 
688
                       setAttribute($attribute, $value[$key]);
Loading history...
688
            }
689
        }
690
691
        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...
692
    }
693
694
    /*
695
    |--------------------------------------------------------------------------
696
    | Pivot help methods
697
    |--------------------------------------------------------------------------
698
    |
699
    | Methods for auditing pivot actions
700
    |
701
    */
702
703
    /**
704
     * @param string $relationName
705
     * @param mixed $id
706
     * @param array $attributes
707
     * @param bool $touch
708
     * @param array $columns
709
     * @param \Closure|null $callback
710
     * @return void
711
     * @throws AuditingException
712
     */
713
    public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'], $callback = null)
714
    {
715
        $this->validateRelationshipMethodExistence($relationName, 'attach');
716
717
        $relationCall = $this->{$relationName}();
718
719
        if ($callback instanceof \Closure) {
720
            $this->applyClosureToRelationship($relationCall, $callback);
721
        }
722
723
        $old = $relationCall->get($columns);
724
        $relationCall->attach($id, $attributes, $touch);
725
        $new = $relationCall->get($columns);
726
727
        $this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new);
728
    }
729
730
    /**
731
     * @param string $relationName
732
     * @param mixed $ids
733
     * @param bool $touch
734
     * @param array $columns
735
     * @param \Closure|null $callback
736
     * @return int
737
     * @throws AuditingException
738
     */
739
    public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'], $callback = null)
740
    {
741
        $this->validateRelationshipMethodExistence($relationName, 'detach');
742
743
        $relationCall = $this->{$relationName}();
744
745
        if ($callback instanceof \Closure) {
746
            $this->applyClosureToRelationship($relationCall, $callback);
747
        }
748
749
        $old = $relationCall->get($columns);
750
        $results = $relationCall->detach($ids, $touch);
751
        $new = $relationCall->get($columns);
752
753
        $this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new);
754
755
        return empty($results) ? 0 : $results;
756
    }
757
758
    /**
759
     * @param string $relationName
760
     * @param Collection|Model|array $ids
761
     * @param bool $detaching
762
     * @param array $columns
763
     * @param \Closure|null $callback
764
     * @return array
765
     * @throws AuditingException
766
     */
767
    public function auditSync(string $relationName, $ids, $detaching = true, $columns = ['*'], $callback = null)
768
    {
769
        $this->validateRelationshipMethodExistence($relationName, 'sync');
770
771
        $relationCall = $this->{$relationName}();
772
773
        if ($callback instanceof \Closure) {
774
            $this->applyClosureToRelationship($relationCall, $callback);
775
        }
776
777
        $old = $relationCall->get($columns);
778
        $changes = $relationCall->sync($ids, $detaching);
779
780
        if (collect($changes)->flatten()->isEmpty()) {
781
            $old = $new = collect([]);
782
        } else {
783
            $new = $relationCall->get($columns);
784
        }
785
786
        $this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new);
787
788
        return $changes;
789
    }
790
791
    /**
792
     * @param string $relationName
793
     * @param Collection|Model|array $ids
794
     * @param array $columns
795
     * @param \Closure|null $callback
796
     * @return array
797
     * @throws AuditingException
798
     */
799
    public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'], $callback = null)
800
    {
801
        $this->validateRelationshipMethodExistence($relationName, 'syncWithoutDetaching');
802
803
        return $this->auditSync($relationName, $ids, false, $columns, $callback);
804
    }
805
806
    /**
807
     * @param string $relationName
808
     * @param Collection|Model|array  $ids
809
     * @param array  $values
810
     * @param bool  $detaching
811
     * @param array $columns
812
     * @param \Closure|null $callback
813
     * @return array
814
     */
815
    public function auditSyncWithPivotValues(string $relationName, $ids, array $values, bool $detaching = true, $columns = ['*'], $callback = null)
816
    {
817
        $this->validateRelationshipMethodExistence($relationName, 'syncWithPivotValues');
818
819
        if ($ids instanceof Model) {
820
            $ids = $ids->getKey();
821
        } elseif ($ids instanceof \Illuminate\Database\Eloquent\Collection) {
822
            $ids = $ids->isEmpty() ? [] : $ids->pluck($ids->first()->getKeyName())->toArray();
823
        } elseif ($ids instanceof Collection) {
824
            $ids = $ids->toArray();
825
        }
826
827
        return $this->auditSync($relationName, collect(Arr::wrap($ids))->mapWithKeys(function ($id) use ($values) {
828
            return [$id => $values];
829
        }), $detaching, $columns, $callback);
830
    }
831
832
    /**
833
     * @param string $relationName
834
     * @param string $event
835
     * @param Collection $old
836
     * @param Collection $new
837
     * @return void
838
     */
839
    private function dispatchRelationAuditEvent($relationName, $event, $old, $new)
840
    {
841
        $this->auditCustomOld[$relationName] = $old->diff($new)->toArray();
842
        $this->auditCustomNew[$relationName] = $new->diff($old)->toArray();
843
844
        if (
845
            empty($this->auditCustomOld[$relationName]) &&
846
            empty($this->auditCustomNew[$relationName])
847
        ) {
848
            $this->auditCustomOld = $this->auditCustomNew = [];
849
        }
850
851
        $this->auditEvent = $event;
852
        $this->isCustomEvent = true;
853
        Event::dispatch(new AuditCustom($this));
0 ignored issues
show
$this of type OwenIt\Auditing\Auditable is incompatible with the type OwenIt\Auditing\Contracts\Auditable expected by parameter $model of OwenIt\Auditing\Events\AuditCustom::__construct(). ( Ignorable by Annotation )

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

853
        Event::dispatch(new AuditCustom(/** @scrutinizer ignore-type */ $this));
Loading history...
854
        $this->auditCustomOld = $this->auditCustomNew = [];
855
        $this->isCustomEvent = false;
856
    }
857
858
    private function validateRelationshipMethodExistence(string $relationName, string $methodName): void
859
    {
860
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), $methodName)) {
861
            throw new AuditingException("Relationship $relationName was not found or does not support method $methodName");
862
        }
863
    }
864
865
    private function applyClosureToRelationship(BelongsToMany $relation, \Closure $closure): void
866
    {
867
        try {
868
            $closure($relation);
869
        } catch (\Throwable $exception) {
870
            throw new AuditingException("Invalid Closure for {$relation->getRelationName()} Relationship");
871
        }
872
    }
873
}
874