Browse Source

model change detector

moell 11 months ago
parent
commit
9a4dff63ec

+ 9 - 1
app/Http/Controllers/API/ProjectController.php

@@ -20,6 +20,7 @@ use App\Http\Resources\API\ProjectKanbanTaskResource;
 use App\Http\Resources\API\SimplePlanResource;
 use App\Http\Resources\API\ProjectRequirementResource;
 use App\Http\Resources\API\ProjectResource;
+use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\Enums\ProjectStatus;
 use App\Models\Enums\TaskStatus;
@@ -33,6 +34,7 @@ use App\Models\Task;
 use App\Models\User;
 use App\Models\RequirementGroup;
 use App\Repositories\ActionRepository;
+use App\Services\History\ModelChangeDetector;
 use App\Services\Project\ProjectKanbanService;
 use App\Services\Project\ProjectGanttService;
 use App\Services\Project\ProjectTaskGroupViewService;
@@ -205,9 +207,15 @@ class ProjectController extends Controller
         $project = Project::findOrFail($id);
 
         $project->status = ProjectStatus::DOING->value;
+        $changes = ModelChangeDetector::detector(ActionObjectType::PROJECT, $project);
         $project->save();
 
-        ActionRepository::createByProject($project, ObjectAction::STARTED, $request->get("comment"));
+        ActionRepository::createByProject(
+            $project,
+            ObjectAction::STARTED,
+            $request->get("comment"),
+            objectChanges: $changes
+        );
 
         return $this->noContent();
     }

+ 15 - 0
app/Models/History.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class History extends Model
+{
+    use HasFactory;
+
+    public $timestamps = false;
+
+    protected $guarded = ['id'];
+}

+ 15 - 4
app/Repositories/ActionRepository.php

@@ -5,7 +5,9 @@ namespace App\Repositories;
 use App\Models\Action;
 use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
+use App\Models\History;
 use App\Models\Project;
+use App\Services\History\ModelChangeDetector;
 use Carbon\Carbon;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Auth;
@@ -18,10 +20,11 @@ class ActionRepository
         ObjectAction $action,
         int|null $projectId = null,
         string $comment = null,
-        array $extraFields = []
+        array $extraFields = [],
+        array $objectChanges = [],
     ): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder
     {
-        return Action::query()->create([
+        $action = Action::query()->create([
             "object_id" => $objectId,
             "object_type" => $objectType->value,
             "action" => $action,
@@ -30,13 +33,20 @@ class ActionRepository
             "extra_fields" => $extraFields ?: null,
             "created_by" => Auth::id(),
         ]);
+
+        if ($objectChanges) {
+            History::query()->insert(array_map(fn($change) => [...$change, 'action_id' => $action->id], $objectChanges));
+        }
+
+        return $action;
     }
 
     public static function createByProject(
         Project $project,
         ObjectAction $action,
         string $comment = null,
-        array $extraFields = []
+        array $extraFields = [],
+        array $objectChanges = [],
     ): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder
     {
         return self::create(
@@ -45,7 +55,8 @@ class ActionRepository
             $action,
             $project->id,
             $comment,
-            $extraFields
+            $extraFields,
+            $objectChanges,
         );
     }
 

+ 8 - 0
app/Services/History/Converter/ConverterContact.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace App\Services\History\Converter;
+
+interface ConverterContact
+{
+    public function handle(mixed $value);
+}

+ 11 - 0
app/Services/History/Converter/WhitelistConverter.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Services\History\Converter;
+
+class WhitelistConverter implements ConverterContact
+{
+    public function handle(mixed $value)
+    {
+        // TODO: Implement handle() method.
+    }
+}

+ 14 - 0
app/Services/History/Detector/DetectorContact.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+use App\Services\History\Converter\ConverterContact;
+
+interface DetectorContact
+{
+    public static function fields(): array;
+
+    public static function diffFields(): array;
+
+    public static function converter(string $field): ConverterContact;
+}

+ 41 - 0
app/Services/History/Detector/ProjectDetector.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+use App\Services\History\Converter\ConverterContact;
+use App\Services\History\Converter\WhitelistConverter;
+
+class ProjectDetector implements DetectorContact
+{
+    public static function fields(): array
+    {
+        return [
+            'name',
+            'code',
+            'const',
+            'status',
+            'begin',
+            'end',
+            'latitude',
+            'type',
+            'acl',
+            'whitelist',
+            'description',
+        ];
+    }
+
+    public static function diffFields(): array
+    {
+        return [
+            'description',
+        ];
+    }
+
+    public static function converter(string $field): ConverterContact
+    {
+        return match ($field) {
+            "whitelist" => new WhitelistConverter(),
+            default => null
+        };
+    }
+}

+ 44 - 0
app/Services/History/ModelChangeDetector.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Services\History;
+
+use App\Models\Enums\ActionObjectType;
+use App\Services\History\Detector\ProjectDetector;
+use Illuminate\Database\Eloquent\Model;
+
+class ModelChangeDetector
+{
+    public static function detector(ActionObjectType $objectType, Model $model): array
+    {
+        $detector = match ($objectType) {
+            ActionObjectType::PROJECT => ProjectDetector::class,
+            default => null
+        };
+
+        if (! $detector) {
+            return [];
+        }
+
+        $fields = call_user_func([$detector, "fields"]);
+        $diffFields = call_user_func([$detector, "diffFields"]);
+
+        if (!$fields) {
+            return [];
+        }
+
+        $items = [];
+        foreach ($fields as $field) {
+            if (! $model->isDirty($field)) {
+                continue;
+            }
+
+            $items[] = [
+                'old' => $model->getOriginal($field),
+                'new' => $model->$field,
+                'diff' => in_array($field, $diffFields) ? text_diff($model->getOriginal($field), $model->$field) : null,
+            ];
+        }
+
+        return $items;
+    }
+}

+ 21 - 0
app/helpers.php

@@ -28,3 +28,24 @@ if (!function_exists('make_tree')) {
         return $tree;
     }
 }
+
+if (!function_exists('text_diff')) {
+    function text_diff(string $text1, string $text2): string
+    {
+        $text1 = str_replace('&nbsp;', '', trim($text1));
+        $text2 = str_replace('&nbsp;', '', trim($text2));
+        $w  = explode("\n", $text1);
+        $o  = explode("\n", $text2);
+        $w1 = array_diff_assoc($w,$o);
+        $o1 = array_diff_assoc($o,$w);
+        $w2 = array();
+        $o2 = array();
+        foreach($w1 as $idx => $val) $w2[sprintf("%03d<",$idx)] = sprintf("%03d- ", $idx+1) . "<del>" . trim($val) . "</del>";
+        foreach($o1 as $idx => $val) $o2[sprintf("%03d>",$idx)] = sprintf("%03d+ ", $idx+1) . "<ins>" . trim($val) . "</ins>";
+        $diff = array_merge($w2, $o2);
+        ksort($diff);
+        return implode("\n", $diff);
+    }
+}
+
+

+ 31 - 0
database/migrations/2024_03_19_201950_create_histories_table.php

@@ -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
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('histories', function (Blueprint $table) {
+            $table->id();
+            $table->bigInteger("action_id")->index();
+            $table->string("field", 100);
+            $table->text("old")->nullable();
+            $table->text("new")->nullable();
+            $table->mediumText("diff")->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('histories');
+    }
+};