owen-it /
laravel-auditing
| 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
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
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
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
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
Loading history...
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
Loading history...
|
|||||||
| 84 | 'audit_updated_at' => $this->serializeDate($this->{$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
Loading history...
|
|||||||
| 85 | 'user_id' => $this->getAttribute($morphPrefix . '_id'), |
||||||
|
0 ignored issues
–
show
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
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
|
|||||||
| 316 | } |
||||||
| 317 | } |
||||||
| 318 |