Audit::getDataValue()   A
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 3
Bugs 0 Features 2
Metric Value
cc 6
eloc 12
c 3
b 0
f 2
nc 4
nop 1
dl 0
loc 25
ccs 0
cts 9
cp 0
crap 42
rs 9.2222
1
<?php
2
3
namespace OwenIt\Auditing;
4
5
use DateTimeInterface;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Carbon;
8
use Illuminate\Support\Facades\Config;
9
use Illuminate\Support\Facades\Date;
10
use Illuminate\Support\Str;
11
use InvalidArgumentException;
12
use OwenIt\Auditing\Contracts\AttributeEncoder;
13
14
trait Audit
15
{
16
    /**
17
     * Audit data.
18
     *
19
     * @var array<string,mixed>
20
     */
21
    protected $data = [];
22
23
    /**
24
     * The Audit attributes that belong to the metadata.
25
     *
26
     * @var array<int,string>
27
     */
28
    protected $metadata = [];
29
30
    /**
31
     * The Auditable attributes that were modified.
32
     *
33
     * @var array<int,string>
34
     */
35
    protected $modified = [];
36
37
    /**
38
     * {@inheritdoc}
39
     */
40
    public function auditable()
41
    {
42
        return $this->morphTo();
0 ignored issues
show
Bug introduced by
It seems like morphTo() 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

42
        return $this->/** @scrutinizer ignore-call */ morphTo();
Loading history...
43
    }
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function user()
49
    {
50
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
51
52
        return $this->morphTo(__FUNCTION__, $morphPrefix . '_type', $morphPrefix . '_id');
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 17
    public function getConnectionName()
59
    {
60 17
        return Config::get('audit.drivers.database.connection');
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 17
    public function getTable(): string
67
    {
68 17
        return Config::get('audit.drivers.database.table', parent::getTable());
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    public function resolveData(): array
75
    {
76
        $morphPrefix = Config::get('audit.user.morph_prefix', 'user');
77
78
        // Metadata
79
        $this->data = [
80
            'audit_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

80
            'audit_id'         => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
81
            'audit_event'      => $this->event,
82
            'audit_tags'       => $this->tags,
83
            'audit_created_at' => $this->serializeDate($this->{$this->getCreatedAtColumn()}),
0 ignored issues
show
Bug introduced by
It seems like serializeDate() 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

83
            'audit_created_at' => $this->/** @scrutinizer ignore-call */ serializeDate($this->{$this->getCreatedAtColumn()}),
Loading history...
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

83
            'audit_created_at' => $this->serializeDate($this->{$this->/** @scrutinizer ignore-call */ getCreatedAtColumn()}),
Loading history...
84
            'audit_updated_at' => $this->serializeDate($this->{$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

84
            'audit_updated_at' => $this->serializeDate($this->{$this->/** @scrutinizer ignore-call */ getUpdatedAtColumn()}),
Loading history...
85
            'user_id'          => $this->getAttribute($morphPrefix . '_id'),
0 ignored issues
show
Bug introduced by
It seems like getAttribute() 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
            'user_id'          => $this->/** @scrutinizer ignore-call */ getAttribute($morphPrefix . '_id'),
Loading history...
86
            'user_type'        => $this->getAttribute($morphPrefix . '_type'),
87
        ];
88
89
        // add resolvers data to metadata
90
        $resolverData = [];
91
        foreach (array_keys(Config::get('audit.resolvers', [])) as $name) {
92
            $resolverData['audit_' . $name] = $this->$name;
93
        }
94
        $this->data = array_merge($this->data, $resolverData);
95
96
        if ($this->user) {
97
            foreach ($this->user->getArrayableAttributes() as $attribute => $value) {
98
                $this->data['user_' . $attribute] = $value;
99
            }
100
        }
101
102
        $this->metadata = array_keys($this->data);
103
104
        // Modified Auditable attributes
105
        foreach ($this->new_values ?? [] as $key => $value) {
106
            $this->data['new_' . $key] = $value;
107
        }
108
109
        foreach ($this->old_values ?? [] as $key => $value) {
110
            $this->data['old_' . $key] = $value;
111
        }
112
113
        $this->modified = array_diff_key(array_keys($this->data), $this->metadata);
114
115
        return $this->data;
116
    }
117
118
    /**
119
     * Get the formatted value of an Eloquent model.
120
     *
121
     * @param mixed $value
122
     *
123
     * @return mixed
124
     */
125
    protected function getFormattedValue(Model $model, string $key, $value)
126
    {
127
        // Apply defined get mutator
128
        if ($model->hasGetMutator($key)) {
129
            return $model->mutateAttribute($key, $value);
130
        }
131
        // hasAttributeMutator since 8.x
132
        // @phpstan-ignore function.alreadyNarrowedType
133
        if (method_exists($model, 'hasAttributeMutator') && $model->hasAttributeMutator($key)) {
134
            return $model->mutateAttributeMarkedAttribute($key, $value);
135
        }
136
137
        if (array_key_exists(
138
            $key,
139
            $model->getCasts()
140
        ) && $model->getCasts()[$key] == 'Illuminate\Database\Eloquent\Casts\AsArrayObject') {
141
            $arrayObject = new \Illuminate\Database\Eloquent\Casts\ArrayObject(json_decode($value, true) ?: []);
142
            return $arrayObject;
143
        }
144
145
        // Cast to native PHP type
146
        if ($model->hasCast($key)) {
147
            if ($model->getCastType($key) == 'datetime' ) {
148
                $value = $this->castDatetimeUTC($model, $value);
149
            }
150
151
            unset($model->classCastCache[$key]);
152
153
            return $model->castAttribute($key, $value);
154
        }
155
156
        // Honour DateTime attribute
157
        if ($value !== null && in_array($key, $model->getDates(), true)) {
158
            return $model->asDateTime($this->castDatetimeUTC($model, $value));
159
        }
160
161
        return $value;
162
    }
163
164
    /**
165
     * @param  Model  $model
166
     * @param  mixed  $value
167
     * @return mixed
168
     */
169
    private function castDatetimeUTC($model, $value)
170
    {
171
        if (!is_string($value)) {
172
            return $value;
173
        }
174
175
        if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value)) {
176
            $date = Carbon::createFromFormat('Y-m-d', $value, Date::now('UTC')->getTimezone());
177
178
            if (! $date) {
179
                return $value;
180
            }
181
182
            return Date::instance($date->startOfDay());
183
        }
184
185
        if (preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $value)) {
186
            return Date::instance(Carbon::createFromFormat('Y-m-d H:i:s', $value, Date::now('UTC')->getTimezone()));
187
        }
188
189
        try {
190
            return Date::createFromFormat($model->getDateFormat(), $value, Date::now('UTC')->getTimezone());
191
        } catch (InvalidArgumentException $e) {
192
            return $value;
193
        }
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function getDataValue(string $key)
200
    {
201
        if (!array_key_exists($key, $this->data)) {
202
            return;
203
        }
204
205
        $value = $this->data[$key];
206
207
        // User value
208
        if ($this->user && Str::startsWith($key, 'user_')) {
209
            return $this->getFormattedValue($this->user, substr($key, 5), $value);
210
        }
211
212
        // Auditable value
213
        if ($this->auditable && Str::startsWith($key, ['new_', 'old_'])) {
214
            $attribute = substr($key, 4);
215
216
            return $this->getFormattedValue(
217
                $this->auditable,
218
                $attribute,
219
                $this->decodeAttributeValue($this->auditable, $attribute, $value)
220
            );
221
        }
222
223
        return $value;
224
    }
225
226
    /**
227
     * Decode attribute value.
228
     *
229
     * @param mixed $value
230
     *
231
     * @return mixed
232
     */
233
    protected function decodeAttributeValue(Contracts\Auditable $auditable, string $attribute, $value)
234
    {
235
        $attributeModifiers = $auditable->getAttributeModifiers();
236
237
        if (!array_key_exists($attribute, $attributeModifiers)) {
238
            return $value;
239
        }
240
241
        $attributeDecoder = $attributeModifiers[$attribute];
242
243
        if (is_subclass_of($attributeDecoder, AttributeEncoder::class)) {
244
            return call_user_func([$attributeDecoder, 'decode'], $value);
245
        }
246
247
        return $value;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function getMetadata(bool $json = false, int $options = 0, int $depth = 512)
254
    {
255
        if (empty($this->data)) {
256
            $this->resolveData();
257
        }
258
259
        $metadata = [];
260
261
        foreach ($this->metadata as $key) {
262
            $value = $this->getDataValue($key);
263
            $metadata[$key] = $value;
264
265
            if ($value instanceof DateTimeInterface) {
266
                $metadata[$key] = !is_null($this->auditable) ? $this->auditable->serializeDate($value) : $this->serializeDate($value);
267
            }
268
        }
269
270
        if (! $json) {
271
            return $metadata;
272
        }
273
274
        return json_encode($metadata, $options, $depth) ?: '{}';
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280
    public function getModified(bool $json = false, int $options = 0, int $depth = 512)
281
    {
282
        if (empty($this->data)) {
283
            $this->resolveData();
284
        }
285
286
        $modified = [];
287
288
        foreach ($this->modified as $key) {
289
            $attribute = substr($key, 4);
290
            $state = substr($key, 0, 3);
291
292
            $value = $this->getDataValue($key);
293
            $modified[$attribute][$state] = $value;
294
295
            if ($value instanceof DateTimeInterface) {
296
                $modified[$attribute][$state] = !is_null($this->auditable) ? $this->auditable->serializeDate($value) : $this->serializeDate($value);
297
            }
298
        }
299
300
        
301
        if (! $json) {
302
            return $modified;
303
        }
304
305
        return json_encode($modified, $options, $depth) ?: '{}';
306
    }
307
308
    /**
309
     * Get the Audit tags as an array.
310
     *
311
     * @return array<string>
312
     */
313
    public function getTags(): array
314
    {
315
        return preg_split('/,/', $this->tags, -1, PREG_SPLIT_NO_EMPTY)?: [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_split('/,/',...IT_NO_EMPTY) ?: array() returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
316
    }
317
}
318