91 - Add activity log table, model and event pipeline
This commit is contained in:
parent
6a9d909525
commit
c7de5bc3df
6 changed files with 249 additions and 0 deletions
39
app/Enums/ActivityTypeEnum.php
Normal file
39
app/Enums/ActivityTypeEnum.php
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ActivityTypeEnum: string
|
||||||
|
{
|
||||||
|
case FETCH = 'fetch';
|
||||||
|
case VALIDATE = 'validate';
|
||||||
|
case APPROVE = 'approve';
|
||||||
|
case REJECT = 'reject';
|
||||||
|
case PUBLISH = 'publish';
|
||||||
|
case ERROR = 'error';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::FETCH => 'Fetched',
|
||||||
|
self::VALIDATE => 'Validated',
|
||||||
|
self::APPROVE => 'Approved',
|
||||||
|
self::REJECT => 'Rejected',
|
||||||
|
self::PUBLISH => 'Published',
|
||||||
|
self::ERROR => 'Error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function options(): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
foreach (self::cases() as $case) {
|
||||||
|
$options[$case->value] = $case->label();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Events/ActivityLogged.php
Normal file
20
app/Events/ActivityLogged.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Enums\ActivityTypeEnum;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
|
||||||
|
class ActivityLogged
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public ActivityTypeEnum $type,
|
||||||
|
public string $message,
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $context = [],
|
||||||
|
public ?Model $subject = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
29
app/Listeners/RecordActivityListener.php
Normal file
29
app/Listeners/RecordActivityListener.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\ActivityLogged;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class RecordActivityListener
|
||||||
|
{
|
||||||
|
private const MESSAGE_LIMIT = 255;
|
||||||
|
|
||||||
|
public function handle(ActivityLogged $event): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
ActivityLog::create([
|
||||||
|
'type' => $event->type,
|
||||||
|
'message' => Str::limit($event->message, self::MESSAGE_LIMIT, ''),
|
||||||
|
'context' => $event->context === [] ? null : $event->context,
|
||||||
|
'subject_type' => $event->subject?->getMorphClass(),
|
||||||
|
'subject_id' => $event->subject?->getKey(),
|
||||||
|
'logged_at' => now(),
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('Failed to record activity: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Models/ActivityLog.php
Normal file
80
app/Models/ActivityLog.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\ActivityTypeEnum;
|
||||||
|
use Database\Factories\ActivityLogFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property ActivityTypeEnum $type
|
||||||
|
* @property string $message
|
||||||
|
* @property array<string, mixed>|null $context
|
||||||
|
* @property string|null $subject_type
|
||||||
|
* @property int|null $subject_id
|
||||||
|
* @property Carbon $logged_at
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class ActivityLog extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<ActivityLogFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'type',
|
||||||
|
'message',
|
||||||
|
'context',
|
||||||
|
'subject_type',
|
||||||
|
'subject_id',
|
||||||
|
'logged_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'type' => ActivityTypeEnum::class,
|
||||||
|
'context' => 'array',
|
||||||
|
'logged_at' => 'datetime',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MorphTo<Model, $this>
|
||||||
|
*/
|
||||||
|
public function subject(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<ActivityLog> $query
|
||||||
|
* @return Builder<ActivityLog>
|
||||||
|
*/
|
||||||
|
public function scopeOfType(Builder $query, ActivityTypeEnum $type): Builder
|
||||||
|
{
|
||||||
|
return $query->where('type', $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<ActivityLog> $query
|
||||||
|
* @return Builder<ActivityLog>
|
||||||
|
*/
|
||||||
|
public function scopeSince(Builder $query, Carbon $since): Builder
|
||||||
|
{
|
||||||
|
return $query->where('logged_at', '>=', $since);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<ActivityLog> $query
|
||||||
|
* @return Builder<ActivityLog>
|
||||||
|
*/
|
||||||
|
public function scopeLatestFirst(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->orderByDesc('logged_at')->orderByDesc('id');
|
||||||
|
}
|
||||||
|
}
|
||||||
50
database/factories/ActivityLogFactory.php
Normal file
50
database/factories/ActivityLogFactory.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\ActivityTypeEnum;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<ActivityLog>
|
||||||
|
*/
|
||||||
|
class ActivityLogFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ActivityLog::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => fake()->randomElement(ActivityTypeEnum::cases()),
|
||||||
|
'message' => fake()->sentence(4),
|
||||||
|
'context' => null,
|
||||||
|
'subject_type' => null,
|
||||||
|
'subject_id' => null,
|
||||||
|
'logged_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function type(ActivityTypeEnum $type): static
|
||||||
|
{
|
||||||
|
return $this->state(['type' => $type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loggedAt(Carbon $loggedAt): static
|
||||||
|
{
|
||||||
|
return $this->state(['logged_at' => $loggedAt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function for_subject(Model $subject): static
|
||||||
|
{
|
||||||
|
return $this->state([
|
||||||
|
'subject_type' => $subject->getMorphClass(),
|
||||||
|
'subject_id' => $subject->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activity_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('type');
|
||||||
|
$table->string('message');
|
||||||
|
$table->json('context')->nullable();
|
||||||
|
$table->string('subject_type')->nullable();
|
||||||
|
$table->unsignedBigInteger('subject_id')->nullable();
|
||||||
|
$table->timestamp('logged_at')->useCurrent();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['type', 'logged_at']);
|
||||||
|
$table->index('logged_at');
|
||||||
|
$table->index(['subject_type', 'subject_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('activity_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue