DetectsChanges::logChanges()   B
last analyzed

Complexity

Conditions 8
Paths 10

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.0069

Importance

Changes 0
Metric Value
cc 8
nc 10
nop 1
dl 0
loc 45
ccs 20
cts 21
cp 0.9524
crap 8.0069
rs 7.9555
c 0
b 0
f 0
1
<?php
2
3
namespace Spatie\Activitylog\Traits;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Str;
8
use Spatie\Activitylog\Exceptions\CouldNotLogChanges;
9
10
trait DetectsChanges
11
{
12
    protected $oldAttributes = [];
13
14 216
    protected static function bootDetectsChanges()
15
    {
16 216
        if (static::eventsToBeRecorded()->contains('updated')) {
17
            static::updating(function (Model $model) {
18
19
                //temporary hold the original attributes on the model
20
                //as we'll need these in the updating event
21 112
                if (method_exists(Model::class, 'getRawOriginal')) {
22
                    // Laravel >7.0
23 112
                    $oldValues = (new static)->setRawAttributes($model->getRawOriginal());
0 ignored issues
show
Bug introduced by
It seems like setRawAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
24 216
                } else {
25
                    // Laravel <7.0
26 216
                    $oldValues = (new static)->setRawAttributes($model->getOriginal());
0 ignored issues
show
Bug introduced by
It seems like setRawAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
27
                }
28 208
29
                $model->oldAttributes = static::logChanges($oldValues);
30 208
            });
31
        }
32 208
    }
33 8
34
    public function attributesToBeLogged(): array
35
    {
36 208
        $attributes = [];
37 4
38
        if (isset(static::$logFillable) && static::$logFillable) {
39
            $attributes = array_merge($attributes, $this->getFillable());
0 ignored issues
show
Bug introduced by
It seems like getFillable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
40 208
        }
41 136
42
        if ($this->shouldLogUnguarded()) {
43 136
            $attributes = array_merge($attributes, array_diff(array_keys($this->getAttributes()), $this->getGuarded()));
0 ignored issues
show
Bug introduced by
It seems like getAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like getGuarded() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
44 32
        }
45
46
        if (isset(static::$logAttributes) && is_array(static::$logAttributes)) {
47
            $attributes = array_merge($attributes, array_diff(static::$logAttributes, ['*']));
48 208
49 8
            if (in_array('*', static::$logAttributes)) {
50
                $attributes = array_merge($attributes, array_keys($this->getAttributes()));
0 ignored issues
show
Bug introduced by
It seems like getAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
51
            }
52 208
        }
53
54
        if (isset(static::$logAttributesToIgnore) && is_array(static::$logAttributesToIgnore)) {
55 140
            $attributes = array_diff($attributes, static::$logAttributesToIgnore);
56
        }
57 140
58 80
        return $attributes;
59
    }
60
61 60
    public function shouldLogOnlyDirty(): bool
62
    {
63
        if (! isset(static::$logOnlyDirty)) {
64 208
            return false;
65
        }
66 208
67 200
        return static::$logOnlyDirty;
68
    }
69
70 8
    public function shouldLogUnguarded(): bool
71
    {
72
        if (! isset(static::$logUnguarded)) {
73
            return false;
74 8
        }
75 4
76
        if (! static::$logUnguarded) {
77
            return false;
78 4
        }
79
80
        if (in_array('*', $this->getGuarded())) {
0 ignored issues
show
Bug introduced by
It seems like getGuarded() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
81 208
            return false;
82
        }
83 208
84 68
        return true;
85
    }
86
87 140
    public function attributeValuesToBeLogged(string $processingEvent): array
88 140
    {
89 140
        if (! count($this->attributesToBeLogged())) {
90 140
            return [];
91
        }
92
93 140
        $properties['attributes'] = static::logChanges(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$properties was never initialized. Although not strictly required by PHP, it is generally a good practice to add $properties = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
94 88
            $processingEvent == 'retrieved'
95
                ? $this
96 88
                : (
97
                    $this->exists
0 ignored issues
show
Bug introduced by
The property exists does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
98 88
                        ? $this->fresh() ?? $this
0 ignored issues
show
Bug introduced by
It seems like fresh() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
99
                        : $this
100
                )
101 140
        );
102 56
103 56
        if (static::eventsToBeRecorded()->contains('updated') && $processingEvent == 'updated') {
104 56
            $nullProperties = array_fill_keys(array_keys($properties['attributes']), null);
105
106 56
            $properties['old'] = array_merge($nullProperties, $this->oldAttributes);
107 20
108
            $this->oldAttributes = [];
109
        }
110 52
111 56
        if ($this->shouldLogOnlyDirty() && isset($properties['old'])) {
112
            $properties['attributes'] = array_udiff_assoc(
113 56
                $properties['attributes'],
114 56
                $properties['old'],
115 56
                function ($new, $old) {
116
                    if ($old === null || $new === null) {
117
                        return $new === $old ? 0 : 1;
118 140
                    }
119
120
                    return $new <=> $old;
121 164
                }
122
            );
123 164
            $properties['old'] = collect($properties['old'])
124 164
                ->only(array_keys($properties['attributes']))
125
                ->all();
126 164
        }
127 140
128 24
        return $properties;
129 140
    }
130 24
131 24
    public static function logChanges(Model $model): array
132 24
    {
133 24
        $changes = [];
134
        $attributes = $model->attributesToBeLogged();
135
136 140
        foreach ($attributes as $attribute) {
137
            if (Str::contains($attribute, '.')) {
138
                $changes += self::getRelatedModelAttributeValue($model, $attribute);
139 140
140 140
                continue;
141
            }
142 32
143 32
            if (Str::contains($attribute, '->')) {
144
                Arr::set(
145
                    $changes,
146
                    str_replace('->', '.', $attribute),
147
                    static::getModelAttributeJsonValue($model, $attribute)
148
                );
149 164
150
                continue;
151
            }
152 24
153
            $changes[$attribute] = $model->getAttribute($attribute);
154 24
155
            if (is_null($changes[$attribute])) {
156
                continue;
157
            }
158 24
159
            if ($model->isDateAttribute($attribute)) {
0 ignored issues
show
Bug introduced by
The method isDateAttribute() cannot be called from this context as it is declared protected in class Illuminate\Database\Eloq...\Concerns\HasAttributes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
160 24
                $changes[$attribute] = $model->serializeDate(
0 ignored issues
show
Bug introduced by
The method serializeDate() cannot be called from this context as it is declared protected in class Illuminate\Database\Eloq...\Concerns\HasAttributes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
161
                    $model->asDateTime($changes[$attribute])
0 ignored issues
show
Bug introduced by
The method asDateTime() cannot be called from this context as it is declared protected in class Illuminate\Database\Eloq...\Concerns\HasAttributes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
162 24
                );
163
            }
164 24
165
            if ($model->hasCast($attribute)) {
166
                $cast = $model->getCasts()[$attribute];
167 24
168
                if ($model->isCustomDateTimeCast($cast)) {
0 ignored issues
show
Bug introduced by
The method isCustomDateTimeCast() cannot be called from this context as it is declared protected in class Illuminate\Database\Eloq...\Concerns\HasAttributes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
169 24
                    $changes[$attribute] = $model->asDateTime($changes[$attribute])->format(explode(':', $cast, 2)[1]);
0 ignored issues
show
Bug introduced by
The method asDateTime() cannot be called from this context as it is declared protected in class Illuminate\Database\Eloq...\Concerns\HasAttributes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
170 24
                }
171 24
            }
172
        }
173 24
174
        return $changes;
175
    }
176
177
    protected static function getRelatedModelAttributeValue(Model $model, string $attribute): array
178
    {
179
        if (substr_count($attribute, '.') > 1) {
180
            throw CouldNotLogChanges::invalidAttribute($attribute);
181
        }
182
183
        [$relatedModelName, $relatedAttribute] = explode('.', $attribute);
0 ignored issues
show
Bug introduced by
The variable $relatedModelName seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $relatedAttribute does not exist. Did you mean $attribute?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
184
185
        $relatedModelName = Str::camel($relatedModelName);
0 ignored issues
show
Bug introduced by
The variable $relatedModelName seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
186
187
        $relatedModel = $model->$relatedModelName ?? $model->$relatedModelName();
188
189
        return ["{$relatedModelName}.{$relatedAttribute}" => $relatedModel->$relatedAttribute ?? null];
0 ignored issues
show
Bug introduced by
The variable $relatedAttribute does not exist. Did you mean $attribute?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
190
    }
191
192
    protected static function getModelAttributeJsonValue(Model $model, string $attribute)
193
    {
194
        $path = explode('->', $attribute);
195
        $modelAttribute = array_shift($path);
196
        $modelAttribute = collect($model->getAttribute($modelAttribute));
197
198
        return data_get($modelAttribute, implode('.', $path));
199
    }
200
}
201