DevOps & Development 4 min de lectura 27 de marzo de 2026

Migrar de PHP Procedural a Laravel: Controladores, CRUD y PKs

Cómo estructurar modelos, rutas y controladores adaptando lógicas procedurales previas.

Solución en "Dos Clics" (TL;DR)

Guía sobre arquitectura MVC y operaciones CRUD en Laravel, adaptando modelos con claves primarias personalizadas y migrando desde PHP procedural.

Dar el salto desde PHP procedural hacia un framework como Laravel puede generar dudas al chocar con patrones y convenciones preestablecidas. Analizar cómo estructurar correctamente las operaciones CRUD, el enrutamiento y la arquitectura Modelo-Vista-Controlador —especialmente cuando se trabaja con esquemas de bases de datos heredados o con claves primarias no autoincrementables— permite adaptar el framework a necesidades específicas sin perder el control del flujo.

1. Contexto del esquema y configuración inicial del modelo

Al migrar sistemas o trabajar con bases de datos ya existentes, es común encontrar tablas con restricciones particulares, como claves primarias que no son autoincrementables. Supongamos una tabla de inventario en MariaDB y su respectivo modelo Eloquent ya mapeado en el proyecto DOSCLIC:

MySQL Console
MariaDB [erp]> desc inventory;
+--------------------+---------------+------+-----+---------+-------+
| Field              | Type          | Null | Key | Default | Extra |
+--------------------+---------------+------+-----+---------+-------+
| idnumber           | bigint(20)    | NO   | PRI | NULL    |       |
| product_id         | bigint(20)    | YES  |     | NULL    |       |
| available_quantity | int(11)       | YES  |     | NULL    |       |
| sale_price         | decimal(10,2) | YES  |     | NULL    |       |
| description        | varchar(250)  | YES  |     | NULL    |       |
| company_id         | bigint(20)    | YES  |     | NULL    |       |
+--------------------+---------------+------+-----+---------+-------+

Para que Laravel no intente tratar la columna idnumber como un autoincremento estándar de enteros y permita gestionar los registros correctamente, el modelo Inventory.php se configura explícitamente de la siguiente manera:

Archivo app/Models/Inventory.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Inventory extends Model
{
    use HasFactory;

    protected $table = 'inventory';

    protected $primaryKey = 'idnumber';
    public $incrementing = false;
    protected $keyType = 'int';
    public $timestamps = false;

    protected $fillable = [
        'product_id',
        'available_quantity',
        'sale_price',
        'description',
        'company_id',
    ];

    public function product()
    {
        return $this->belongsTo(Product::class, 'product_id', 'idnumber');
    }

    public function company()
    {
        return $this->belongsTo(Company::class, 'company_id', 'idnumber');
    }

2. Transición de PHP Procedural a Laravel MVC

En metodologías procedurales tradicionales (similares a arquitecturas tipo Moodle), es común unificar la lógica de creación y edición en un solo archivo (por ejemplo, edit.php?id=...), utilizando marcas de tiempo Unix generadas con time() como clave primaria y validando mediante condicionales si el registro existe para decidir un INSERT o un UPDATE.

Laravel desacopla este flujo mediante métodos HTTP semánticos dentro de un controlador de recursos (Resource Controller). Sin embargo, es perfectamente viable mantener la generación manual de la clave primaria si la lógica de negocio lo requiere, asistiéndonos de Artisan para generar el controlador base:

Terminal (SSH)
php artisan make:controller InventoryController --resource --model=Inventory

3. El Controlador y la Generación Manual de Claves Primarias

Debido a que la clave primaria idnumber no es autoincrementable por la base de datos, el controlador debe asignar explícitamente el valor único antes de invocar el método de creación (create). Aquí es donde evaluamos el uso de time() frente a alternativas para evitar colisiones sin perder la legibilidad temporal.

Error detectado: Si intentas guardar un modelo Eloquent con una clave primaria personalizada vacía o no autoincrementable, la base de datos rechazará la inserción por violación de restricciones de clave primaria (Null or Duplicate entry).

Implementación del controlador InventoryController.php gestionando la validación y la asignación manual del identificador:

Archivo app/Http/Controllers/InventoryController.php
<?php

namespace App\Http\Controllers;

use App\Models\Inventory;
use Illuminate\Http\Request;

class InventoryController extends Controller
{
    public function index()
    {
        $inventories = Inventory::all();
        return view('inventory.index', compact('inventories'));
    }

    public function create()
    {
        return view('inventory.create');
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'product_id' => 'required|integer',
            'available_quantity' => 'required|integer',
            'sale_price' => 'required|numeric',
            'description' => 'nullable|string',
            'company_id' => 'required|integer',
        ]);

        $validated['idnumber'] = time();
        Inventory::create($validated);

        return redirect()->route('inventory.index')->with('success', 'Inventario creado correctamente');
    }

    public function show(Inventory $inventory)
    {
        return view('inventory.show', compact('inventory'));
    }

    public function edit(Inventory $inventory)
    {
        return view('inventory.edit', compact('inventory'));
    }

    public function update(Request $request, Inventory $inventory)
    {
        $validated = $request->validate([
            'product_id' => 'required|integer',
            'available_quantity' => 'required|integer',
            'sale_price' => 'required|numeric',
            'description' => 'nullable|string',
            'company_id' => 'required|integer',
        ]);

        $inventory->update($validated);

        return redirect()->route('inventory.index')->with('success', 'Inventario actualizado correctamente');
    }

    public function destroy(Inventory $inventory)
    {
        $inventory->delete();
        return redirect()->route('inventory.index')->with('success', 'Inventario eliminado correctamente');
    }
}

4. Rutas y Consideraciones sobre Timestamps

Para registrar todas las acciones del controlador de recursos en el sistema de enrutamiento de Laravel, basta con declarar una sola línea en el archivo de rutas web:

Archivo routes/web.php
use App\Http\Controllers\InventoryController;

Route::resource('inventory', InventoryController::class);
Consejo Práctico: Usar time() como clave primaria facilita formatear y revertir el identificador a un formato de fecha legible mediante date('d/m/Y', $timestamp). Sin embargo, si existe riesgo de inserciones concurrentes en el mismo segundo, considera usar milisegundos multiplicando microtime(true) * 1000 y dividiendo entre 1000 al momento de formatear la fecha.
La historia detrás de la nota

Aprender haciendo implica chocar con las convenciones estrictas de los frameworks modernos y entender cómo adaptarlas a estructuras heredadas sin romper el diseño base.