2023-11-13 11:56:06 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
use App\Enums\ActivityLogs\ActivityLogsEventEnum;
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2024-02-12 15:51:31 +01:00
|
|
|
use Illuminate\Support\Str;
|
2023-11-13 11:56:06 +01:00
|
|
|
|
|
|
|
|
/**
|
2024-02-12 15:51:31 +01:00
|
|
|
* @property string $model Target model.
|
|
|
|
|
* @property string $model_id Id of the target model.
|
|
|
|
|
* @property \App\Enums\ActivityLogs\ActivityLogsEventEnum $event Event of this activity (ActivityLogsEventEnum).
|
2026-04-24 15:04:18 +02:00
|
|
|
* @property array<string, mixed> $data Changes.
|
|
|
|
|
* @property-read \Illuminate\Support\Carbon $created_at Created date.
|
2023-11-13 11:56:06 +01:00
|
|
|
*
|
|
|
|
|
* @property-read \App\Models\User $user User BelongsTo relation.
|
|
|
|
|
*/
|
|
|
|
|
class ActivityLog extends Model
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Indicates if the model should be timestamped.
|
|
|
|
|
*
|
|
|
|
|
* @var boolean
|
|
|
|
|
*/
|
|
|
|
|
public $timestamps = false;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The attributes that are mass assignable.
|
|
|
|
|
*
|
2026-04-24 15:04:18 +02:00
|
|
|
* @var list<string>
|
2023-11-13 11:56:06 +01:00
|
|
|
*/
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
'is_anonymous',
|
|
|
|
|
'model_class',
|
|
|
|
|
'model_id',
|
|
|
|
|
'event',
|
|
|
|
|
'data',
|
|
|
|
|
'created_at',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The attributes that should be cast.
|
|
|
|
|
*
|
|
|
|
|
* @var array<string, string>
|
|
|
|
|
*/
|
|
|
|
|
protected $casts = [
|
|
|
|
|
'is_anonymous' => 'bool',
|
|
|
|
|
'event' => ActivityLogsEventEnum::class,
|
|
|
|
|
'data' => 'json',
|
|
|
|
|
'created_at' => 'datetime',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// * METHODS
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add new activity to the activity logs list.
|
|
|
|
|
*
|
|
|
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
|
|
|
* @param \App\Enums\ActivityLogs\ActivityLogsEventEnum $eventEnum
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public static function addActivity(Model $model, ActivityLogsEventEnum $eventEnum): void
|
|
|
|
|
{
|
|
|
|
|
$isAnonymous = true;
|
|
|
|
|
/** @var \Illuminate\Database\Eloquent\Model|null */
|
|
|
|
|
$userModel = auth('backend')->user();
|
|
|
|
|
if (is_object($userModel)) {
|
|
|
|
|
$userModel = $userModel->getKeyForSelectQuery();
|
|
|
|
|
$isAnonymous = false;
|
|
|
|
|
}
|
|
|
|
|
$activity = new self();
|
|
|
|
|
$activity->user_id = $userModel;
|
|
|
|
|
$activity->is_console = app()->runningInConsole();
|
|
|
|
|
$activity->is_anonymous = $isAnonymous;
|
|
|
|
|
$activity->model_class = \get_class($model);
|
|
|
|
|
$activity->model_id = $model->getKey();
|
|
|
|
|
$activity->event = $eventEnum;
|
|
|
|
|
$activity->data = static::getChangedColumns($activity, $model, $eventEnum);
|
2026-04-24 15:04:18 +02:00
|
|
|
// @phpstan-ignore-next-line
|
|
|
|
|
$activity->created_at = now();
|
2023-11-13 11:56:06 +01:00
|
|
|
$activity->saveOrFail();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get old, new columns that has changed.
|
|
|
|
|
*
|
2024-02-12 15:51:31 +01:00
|
|
|
* @param self $activity
|
2023-11-13 11:56:06 +01:00
|
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
|
|
|
* @param \App\Enums\ActivityLogs\ActivityLogsEventEnum $eventEnum
|
2026-04-24 15:04:18 +02:00
|
|
|
* @return array<string, string>
|
2023-11-13 11:56:06 +01:00
|
|
|
*/
|
2024-02-12 15:51:31 +01:00
|
|
|
public static function getChangedColumns(self $activity, Model $model, ActivityLogsEventEnum $eventEnum): array
|
2023-11-13 11:56:06 +01:00
|
|
|
{
|
|
|
|
|
$modelClassName = $activity->model_class;
|
2026-04-24 15:04:18 +02:00
|
|
|
$modelTarget = new $modelClassName();
|
|
|
|
|
|
2023-11-13 11:56:06 +01:00
|
|
|
/** Get type of each fields of the target model */
|
2024-02-12 15:51:31 +01:00
|
|
|
/** @var \Illuminate\Database\Eloquent\Model|null $targetModel */
|
2026-04-24 15:04:18 +02:00
|
|
|
$targetModel = ($modelTarget instanceof \Illuminate\Database\Eloquent\Model) ? $activity->model_class::where(
|
|
|
|
|
$modelTarget->getRouteKeyName(),
|
2023-11-13 11:56:06 +01:00
|
|
|
$activity->model_id
|
2026-04-24 15:04:18 +02:00
|
|
|
)->first() : null;
|
2024-02-12 15:51:31 +01:00
|
|
|
|
|
|
|
|
if (\is_null($targetModel)) {
|
|
|
|
|
return [];
|
2023-11-13 11:56:06 +01:00
|
|
|
}
|
2024-02-12 15:51:31 +01:00
|
|
|
$targetModelTypes = collect($targetModel->toArray())
|
|
|
|
|
->map(fn ($value) => self::getValueType($value))
|
|
|
|
|
->toArray();
|
2023-11-13 11:56:06 +01:00
|
|
|
/** Return old, new and type of values changed */
|
2024-02-12 15:51:31 +01:00
|
|
|
return match ($eventEnum) {
|
|
|
|
|
ActivityLogsEventEnum::created => array_intersect_key($targetModelTypes, $model->toArray()),
|
|
|
|
|
ActivityLogsEventEnum::duplicated => array_intersect_key($targetModelTypes, $model->toArray()),
|
|
|
|
|
ActivityLogsEventEnum::updated => array_intersect_key($targetModelTypes, $model->getChanges()),
|
|
|
|
|
default => []
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a value type as a string
|
2024-03-20 17:39:24 +01:00
|
|
|
* ! Use this for display purpose only.
|
2024-02-12 15:51:31 +01:00
|
|
|
*
|
|
|
|
|
* @param mixed $value
|
|
|
|
|
* @return string
|
|
|
|
|
* @throws \RuntimeException If type is unhlanded.
|
|
|
|
|
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
|
|
|
|
|
*/
|
|
|
|
|
private static function getValueType(mixed $value): string
|
|
|
|
|
{
|
|
|
|
|
// phpcs:enable
|
|
|
|
|
$type = \gettype($value);
|
|
|
|
|
switch ($type) {
|
2026-04-24 15:04:18 +02:00
|
|
|
// phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
|
2024-02-12 15:51:31 +01:00
|
|
|
case 'string':
|
|
|
|
|
return match (true) {
|
2024-03-20 17:39:24 +01:00
|
|
|
Str::length(\strip_tags($value)) !== Str::length($value) => 'html',
|
|
|
|
|
Str::startsWith($value, 'storage/modelfiles') => 'file',
|
|
|
|
|
Str::isUuid($value) => 'uuid',
|
|
|
|
|
Str::isUlid($value) => 'ulid',
|
|
|
|
|
Str::isUrl($value) => 'url',
|
|
|
|
|
Str::isMatch('/^[0-9A-Za-z_-]{10}[048AEIMQUYcgkosw]$/', $value) => 'youtube_video_id',
|
|
|
|
|
\is_numeric($value) => 'numeric',
|
2024-02-12 15:51:31 +01:00
|
|
|
Str::isJson($value) and
|
|
|
|
|
(Str::startsWith($value, '[') or Str::startsWith($value, '{')) => 'json',
|
|
|
|
|
default => 'string'
|
|
|
|
|
};
|
|
|
|
|
case 'NULL':
|
|
|
|
|
case 'object':
|
|
|
|
|
case 'array':
|
|
|
|
|
case 'integer':
|
|
|
|
|
case 'boolean':
|
|
|
|
|
return $type;
|
|
|
|
|
case 'double':
|
|
|
|
|
return is_float($value) ? 'float' : 'double';
|
|
|
|
|
case 'resource (closed)':
|
|
|
|
|
case 'resource':
|
|
|
|
|
throw new \RuntimeException('Laravel model properties should not contain resources !');
|
|
|
|
|
case 'unknown type':
|
|
|
|
|
default:
|
|
|
|
|
throw new \RuntimeException('Unhandled Type ' . \gettype($value));
|
|
|
|
|
} //end switch
|
2023-11-13 11:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 15:04:18 +02:00
|
|
|
// * RELATIONS.
|
2023-11-13 11:56:06 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* User BelongsTo relation.
|
|
|
|
|
*
|
2026-04-24 15:04:18 +02:00
|
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User,$this>
|
2023-11-13 11:56:06 +01:00
|
|
|
*/
|
2024-02-12 15:51:31 +01:00
|
|
|
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
2023-11-13 11:56:06 +01:00
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class);
|
|
|
|
|
}
|
|
|
|
|
}
|