Refactor Prop model to use enum for data types; remove PropDataType model and update related requests and migrations

This commit is contained in:
2026-06-22 13:13:22 -03:00
parent 7075704dad
commit 55694b2048
12 changed files with 454 additions and 70 deletions

View File

@@ -2,11 +2,11 @@
namespace App\Domains\Prop\Models;
use App\Domains\Prop\Support\PropDataType;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
@@ -14,12 +14,23 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
'codigo',
'nombre',
'is_required',
'data_type_id',
'data_type',
])]
class Prop extends Model
{
use HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_required' => 'boolean',
'data_type' => PropDataType::class,
];
}
/**
* @param Builder<self> $query
*/
@@ -28,14 +39,6 @@ class Prop extends Model
$query->where('propable_type', is_string($model) ? $model : $model::class);
}
/**
* @return BelongsTo<PropDataType, $this>
*/
public function dataType(): BelongsTo
{
return $this->belongsTo(PropDataType::class, 'data_type_id');
}
/**
* @return HasMany<PropOption, $this>
*/

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Domains\Prop\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'name',
'code',
])]
class PropDataType extends Model
{
use HasFactory;
/**
* @return HasMany<Prop, $this>
*/
public function props(): HasMany
{
return $this->hasMany(Prop::class, 'data_type_id');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Prop\Requests;
use App\Domains\Prop\Support\PropDataType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -21,7 +22,7 @@ class StorePropRequest extends FormRequest
'codigo' => ['required', 'string', 'max:255', Rule::unique('props', 'codigo')],
'nombre' => ['required', 'string', 'max:255'],
'is_required' => ['sometimes', 'boolean'],
'data_type_id' => ['required', 'integer', 'exists:props_data_types,id'],
'data_type' => ['required', Rule::enum(PropDataType::class)],
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Prop\Requests;
use App\Domains\Prop\Models\Prop;
use App\Domains\Prop\Support\PropDataType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -25,7 +26,7 @@ class UpdatePropRequest extends FormRequest
'codigo' => ['required', 'string', 'max:255', Rule::unique('props', 'codigo')->ignore($prop?->id)],
'nombre' => ['required', 'string', 'max:255'],
'is_required' => ['sometimes', 'boolean'],
'data_type_id' => ['required', 'integer', 'exists:props_data_types,id'],
'data_type' => ['required', Rule::enum(PropDataType::class)],
];
}
}

View File

@@ -2,11 +2,13 @@
namespace App\Domains\Prop\Resources;
use App\Domains\Prop\Models\Prop;
use App\Domains\Prop\Support\PropableModels;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Domains\Prop\Models\Prop
* @mixin Prop
*/
class PropResource extends JsonResource
{
@@ -17,11 +19,11 @@ class PropResource extends JsonResource
{
return [
'id' => $this->id,
'propable_type' => $this->propable_type,
'propable_type' => PropableModels::aliasFor($this->propable_type) ?? $this->propable_type,
'codigo' => $this->codigo,
'nombre' => $this->nombre,
'is_required' => $this->is_required,
'data_type_id' => $this->data_type_id,
'data_type' => $this->data_type?->value,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Prop\Support;
enum PropDataType: string
{
case String = 'string';
case Integer = 'integer';
case Decimal = 'decimal';
case Boolean = 'boolean';
case Date = 'date';
case DateTime = 'datetime';
case Json = 'json';
case Select = 'select';
/**
* @return list<string>
*/
public static function values(): array
{
return array_map(
static fn (self $type): string => $type->value,
self::cases(),
);
}
}

View File

@@ -39,4 +39,9 @@ class PropableModels
return $modelClass;
}
public static function aliasFor(string $modelClass): ?string
{
return array_search($modelClass, static::map(), true) ?: null;
}
}

View File

@@ -1,29 +0,0 @@
<?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('props_data_types', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('props_data_types');
}
};

View File

@@ -17,7 +17,7 @@ return new class extends Migration
$table->string('codigo')->unique();
$table->string('nombre');
$table->boolean('is_required')->default(false);
$table->foreignId('data_type_id')->constrained('props_data_types')->cascadeOnUpdate()->restrictOnDelete();
$table->string('data_type');
$table->timestamps();
$table->index('propable_type');

View File

@@ -0,0 +1,106 @@
<?php
use App\Domains\Prop\Support\PropDataType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable('props')) {
return;
}
if (Schema::hasColumn('props', 'data_type_id')) {
Schema::table('props', function (Blueprint $table) {
$table->string('data_type')->nullable()->after('is_required');
});
if (Schema::hasTable('props_data_types')) {
$typeMap = DB::table('props_data_types')
->pluck('code', 'id')
->map(static fn (mixed $code): string => in_array($code, PropDataType::values(), true) ? (string) $code : PropDataType::String->value)
->all();
DB::table('props')
->select(['id', 'data_type_id'])
->orderBy('id')
->get()
->each(function (object $prop) use ($typeMap): void {
DB::table('props')
->where('id', $prop->id)
->update([
'data_type' => $typeMap[$prop->data_type_id] ?? PropDataType::String->value,
]);
});
} else {
DB::table('props')->update([
'data_type' => PropDataType::String->value,
]);
}
Schema::table('props', function (Blueprint $table) {
$table->dropForeign(['data_type_id']);
$table->dropColumn('data_type_id');
});
Schema::table('props', function (Blueprint $table) {
$table->string('data_type')->nullable(false)->change();
});
}
if (Schema::hasTable('props_data_types')) {
Schema::drop('props_data_types');
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (! Schema::hasTable('props_data_types')) {
Schema::create('props_data_types', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->timestamps();
});
DB::table('props_data_types')->insert([
[
'id' => 1,
'name' => 'String',
'code' => PropDataType::String->value,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
if (Schema::hasTable('props') && Schema::hasColumn('props', 'data_type') && ! Schema::hasColumn('props', 'data_type_id')) {
Schema::table('props', function (Blueprint $table) {
$table->foreignId('data_type_id')->nullable()->after('is_required')->constrained('props_data_types')->cascadeOnUpdate()->restrictOnDelete();
});
$stringTypeId = DB::table('props_data_types')
->where('code', PropDataType::String->value)
->value('id');
DB::table('props')->update([
'data_type_id' => $stringTypeId,
]);
Schema::table('props', function (Blueprint $table) {
$table->foreignId('data_type_id')->nullable(false)->change();
$table->dropColumn('data_type');
});
}
}
};

View File

@@ -0,0 +1,249 @@
{
"info": {
"_postman_id": "b0b0df2c-4e1e-4ef4-b6a8-1c5348d5b6f2",
"name": "Shopit Back - Tenants and Props",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "base_url",
"value": "http://127.0.0.1:8000/api"
},
{
"key": "tenant_id",
"value": "1"
},
{
"key": "prop_id",
"value": "1"
},
{
"key": "propable_type",
"value": "tenant"
}
],
"item": [
{
"name": "Tenants",
"item": [
{
"name": "List Tenants",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/tenants",
"host": [
"{{base_url}}"
],
"path": [
"tenants"
]
}
}
},
{
"name": "Create Tenant",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"tenant-demo\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"tenant-demo.local\",\n \"props\": {\n \"color_primario\": \"#ff6600\",\n \"pais\": \"AR\"\n }\n}"
},
"url": {
"raw": "{{base_url}}/tenants",
"host": [
"{{base_url}}"
],
"path": [
"tenants"
]
}
}
},
{
"name": "Show Tenant",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/tenants/{{tenant_id}}",
"host": [
"{{base_url}}"
],
"path": [
"tenants",
"{{tenant_id}}"
]
}
}
},
{
"name": "Update Tenant",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"tenant-demo\",\n \"nombre\": \"Tenant Demo Updated\",\n \"dominio\": \"tenant-demo.example.com\",\n \"props\": {\n \"color_primario\": \"#0055ff\",\n \"pais\": \"UY\"\n }\n}"
},
"url": {
"raw": "{{base_url}}/tenants/{{tenant_id}}",
"host": [
"{{base_url}}"
],
"path": [
"tenants",
"{{tenant_id}}"
]
}
}
},
{
"name": "Delete Tenant",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{base_url}}/tenants/{{tenant_id}}",
"host": [
"{{base_url}}"
],
"path": [
"tenants",
"{{tenant_id}}"
]
}
}
}
]
},
{
"name": "Props",
"item": [
{
"name": "List Props By Model",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/prop-models/{{propable_type}}/props",
"host": [
"{{base_url}}"
],
"path": [
"prop-models",
"{{propable_type}}",
"props"
]
}
}
},
{
"name": "Create Prop For Model",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"color_primario\",\n \"nombre\": \"Color primario\",\n \"is_required\": false,\n \"data_type\": \"string\"\n}"
},
"url": {
"raw": "{{base_url}}/prop-models/{{propable_type}}/props",
"host": [
"{{base_url}}"
],
"path": [
"prop-models",
"{{propable_type}}",
"props"
]
}
}
},
{
"name": "Show Prop",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}",
"host": [
"{{base_url}}"
],
"path": [
"prop-models",
"{{propable_type}}",
"props",
"{{prop_id}}"
]
}
}
},
{
"name": "Update Prop",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"color_primario\",\n \"nombre\": \"Color primario actualizado\",\n \"is_required\": true,\n \"data_type\": \"string\"\n}"
},
"url": {
"raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}",
"host": [
"{{base_url}}"
],
"path": [
"prop-models",
"{{propable_type}}",
"props",
"{{prop_id}}"
]
}
}
},
{
"name": "Delete Prop",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}",
"host": [
"{{base_url}}"
],
"path": [
"prop-models",
"{{propable_type}}",
"props",
"{{prop_id}}"
]
}
}
}
]
}
]
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Tests\Feature\Prop;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PropControllerTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_a_prop_using_enum_data_type(): void
{
$response = $this->postJson('/api/prop-models/tenant/props', [
'codigo' => 'color_primario',
'nombre' => 'Color primario',
'is_required' => false,
'data_type' => 'string',
]);
$response
->assertCreated()
->assertJsonPath('propable_type', 'tenant')
->assertJsonPath('data_type', 'string');
$this->assertDatabaseHas('props', [
'codigo' => 'color_primario',
'data_type' => 'string',
]);
}
public function test_it_rejects_unknown_enum_data_type(): void
{
$response = $this->postJson('/api/prop-models/tenant/props', [
'codigo' => 'color_primario',
'nombre' => 'Color primario',
'is_required' => false,
'data_type' => 'unsupported-type',
]);
$response
->assertUnprocessable()
->assertJsonValidationErrors(['data_type']);
}
}