Curso de Laravel

CRUD en Laravel: crear registros desde un formulario

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Continuando con el CRUD, hoy veremos la C de crear: el formulario y el guardado.

¡Empecemos!

Los dos métodos

El CRUD separa esto en dos pasos, y entender por qué evita confusiones:

  • create() responde a GET y muestra el formulario.
  • store() responde a POST y guarda los datos.

Son dos peticiones distintas a la misma dirección con métodos diferentes. La primera no modifica nada; la segunda sí.

El método create

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

Cuando el formulario necesita datos —para llenar un desplegable, por ejemplo— se pasan aquí:

public function create()
{
    $doctores = Doctor::where('activo', true)
        ->orderBy('apellido')
        ->pluck('nombre', 'id');

    return view('citas.create', compact('doctores'));
}

Ese pluck('nombre', 'id') devuelve directamente el par clave-valor que necesita un <select>.

El formulario

@extends('layouts.app')

@section('titulo', 'Nuevo paciente')

@section('contenido')
    <h1>Registrar paciente</h1>

    <form method="POST" action="{{ route('pacientes.store') }}">
        @csrf

        <x-campo nombre="cedula" etiqueta="Cédula" requerido />
        <x-campo nombre="nombre" etiqueta="Nombre" requerido />
        <x-campo nombre="apellido" etiqueta="Apellido" requerido />
        <x-campo nombre="fecha_nacimiento" etiqueta="Fecha de nacimiento"
                 tipo="date" requerido />
        <x-campo nombre="telefono" etiqueta="Teléfono" />
        <x-campo nombre="correo" etiqueta="Correo" tipo="email" />

        <div class="acciones">
            <button type="submit">Guardar</button>
            <a href="{{ route('pacientes.index') }}">Cancelar</a>
        </div>
    </form>
@endsection

Aquí se ve el valor del componente de campo: seis campos en seis líneas, con etiqueta, old() y mensaje de error resueltos dentro.

@csrf es obligatorio. Sin él, Laravel responde con un error 419.

El método store

public function store(GuardarPacienteRequest $request)
{
    $paciente = Paciente::create($request->validated());

    return redirect()
        ->route('pacientes.show', $paciente)
        ->with('exito', 'Paciente registrado correctamente');
}

Tres líneas, y hace todo lo necesario: valida, guarda y redirige con un mensaje.

La validación ocurre antes de entrar al método, gracias al Form Request. Si llegas aquí, los datos son correctos.

POST-Redirect-GET

Fíjate en que el método redirige en lugar de devolver una vista. No es un detalle estético.

Si devolvieras la vista directamente, el navegador se quedaría en una URL que respondió a un POST. Cuando el usuario recargue, el navegador preguntará si quiere reenviar los datos, y si dice que sí, se crea un registro duplicado.

Es el patrón que vimos en el curso de PHP: tras guardar, siempre redirigir.

Mostrar el mensaje

El with('exito', ...) guarda el mensaje para la siguiente petición. En el layout:

@if (session('exito'))
    <div class="alerta-exito">{{ session('exito') }}</div>
@endif

Al estar en el layout, cualquier redirección con mensaje lo muestra automáticamente, sin repetir el bloque en cada vista.

Campos relacionados

Cuando el formulario incluye una clave foránea:

<div class="campo">
    <label for="doctor_id">Doctor</label>
    <select name="doctor_id" id="doctor_id" required>
        <option value="">Selecciona un doctor</option>
        @foreach ($doctores as $id => $nombre)
            <option value="{{ $id }}" @selected(old('doctor_id') == $id)>
                {{ $nombre }}
            </option>
        @endforeach
    </select>

    @error('doctor_id')
        <span class="mensaje-error">{{ $message }}</span>
    @enderror
</div>

La directiva @selected() conserva la opción elegida si la validación falla.

Y la regla de validación correspondiente:

'doctor_id' => ['required', 'exists:doctores,id'],

exists importa. Sin ella, alguien podría modificar el HTML y enviar un id que no existe, o el de un doctor desactivado.

Crear con relaciones

Si el registro pertenece a otro, créalo desde la relación:

public function store(GuardarCitaRequest $request, Paciente $paciente)
{
    $cita = $paciente->citas()->create($request->validated());

    return redirect()
        ->route('citas.show', $cita)
        ->with('exito', 'Cita agendada');
}

Eso asigna el paciente_id automáticamente, sin que puedas equivocarte ni que nadie pueda falsificarlo desde el formulario.

Asignar el usuario actual

Un caso muy frecuente:

public function store(GuardarCitaRequest $request)
{
    $cita = Cita::create([
        ...$request->validated(),
        'creado_por' => auth()->id(),
    ]);

    return redirect()->route('citas.index')->with('exito', 'Cita agendada');
}

Nunca aceptes el creado_por desde el formulario. Ese dato lo pone el servidor.

Varias operaciones a la vez

Cuando crear un registro implica más de una escritura, envuélvelo en una transacción:

public function store(GuardarCitaRequest $request)
{
    $cita = DB::transaction(function () use ($request) {
        $cita = Cita::create($request->validated());

        $cita->estudios()->attach($request->estudios ?? []);

        $cita->paciente->increment('total_citas');

        return $cita;
    });

    return redirect()->route('citas.show', $cita)->with('exito', 'Cita agendada');
}

Si algo falla a mitad, se revierte todo. Es la idea de atomicidad que vimos en el curso de MySQL.

Guardar y seguir creando

Un detalle que agradecen los usuarios que cargan muchos registros seguidos:

<button type="submit" name="accion" value="guardar">Guardar</button>
<button type="submit" name="accion" value="guardar_y_nuevo">Guardar y crear otro</button>
if ($request->accion === 'guardar_y_nuevo') {
    return redirect()
        ->route('pacientes.create')
        ->with('exito', 'Paciente registrado. Puedes crear otro.');
}

return redirect()->route('pacientes.show', $paciente);

Parece menor, pero para quien carga cincuenta registros en una sesión cambia bastante el trabajo.

Evitar el envío doble

Un usuario impaciente puede pulsar «Guardar» dos veces. Además de la protección con transacción, conviene desactivar el botón:

<button type="submit" onclick="this.disabled=true; this.form.submit();">
    Guardar
</button>

Y para casos donde no puede haber duplicados, una restricción unique en la base de datos es la única garantía real. La validación puede fallar en una carrera entre dos peticiones simultáneas; la base de datos, no.

Errores comunes

  • Olvidar @csrf y recibir un 419.
  • Devolver una vista en lugar de redirigir tras el POST.
  • Usar $request->all() en lugar de validated().
  • Aceptar la clave foránea del usuario sin exists.
  • Poner creado_por en el formulario en vez de asignarlo en el servidor.
  • Varias escrituras sin transacción.

Para cerrar

Crear un registro son dos métodos y un formulario, pero hay tres cosas que no se pueden saltar: @csrf, validar con Form Request y redirigir después de guardar.

En la siguiente lección veremos la edición, que reutiliza casi todo lo de hoy.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp