Curso de Laravel

CRUD en Laravel: actualizar registros

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Continuando con el CRUD, hoy veremos la U de actualizar.

Es muy parecido a crear, pero con tres diferencias que conviene tener claras porque son las que hacen fallar a la gente.

¡Empecemos!

Los dos métodos

  • edit() responde a GET y muestra el formulario con los datos actuales.
  • update() responde a PUT o PATCH y guarda los cambios.

El método edit

public function edit(Paciente $paciente)
{
    return view('pacientes.edit', compact('paciente'));
}

Gracias a la vinculación de modelos que vimos en la lección de rutas, el paciente llega ya resuelto. Si no existe, Laravel devuelve un 404 sin que hagas nada.

Y si el formulario necesita datos adicionales:

public function edit(Cita $cita)
{
    $doctores = Doctor::where('activo', true)->pluck('nombre', 'id');

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

El formulario: la primera diferencia

<form method="POST" action="{{ route('pacientes.update', $paciente) }}">
    @csrf
    @method('PUT')

    <x-campo nombre="cedula" etiqueta="Cédula" :valor="$paciente->cedula" requerido />
    <x-campo nombre="nombre" etiqueta="Nombre" :valor="$paciente->nombre" requerido />
    <x-campo nombre="apellido" etiqueta="Apellido" :valor="$paciente->apellido" requerido />
    <x-campo nombre="telefono" etiqueta="Teléfono" :valor="$paciente->telefono" />

    <button type="submit">Actualizar</button>
</form>

@method('PUT') es lo primero que conviene retener. Los navegadores solo saben enviar GET y POST, así que Laravel usa un campo oculto para simular PUT. Sin esa línea, la petición llega como POST y no encuentra la ruta: error 405.

Y fíjate en los dos puntos: :valor="$paciente->cedula" pasa el valor de la propiedad; sin ellos pasaría el texto literal.

El orden de old() y el valor actual

Dentro del componente de campo tenemos:

value="{{ old($nombre, $valor) }}"

Ese orden es importante y explica un comportamiento que confunde:

  • La primera vez que se carga el formulario no hay old(), así que muestra el valor actual del registro.
  • Si la validación falla, old() tiene lo que el usuario escribió y eso es lo que se muestra.

Al revés, el usuario perdería sus correcciones cada vez que fallara la validación.

El método update

public function update(ActualizarPacienteRequest $request, Paciente $paciente)
{
    $paciente->update($request->validated());

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

Igual que al crear: valida, guarda y redirige.

La regla unique: la segunda diferencia

Esta es la que más problemas da.

'cedula' => ['required', 'unique:pacientes'],

Al editar, esa regla siempre falla: el propio registro ya tiene esa cédula, así que la validación la considera duplicada aunque el usuario no la haya tocado.

Hay que excluir el registro actual:

use Illuminate\Validation\Rule;

public function rules(): array
{
    return [
        'cedula' => [
            'required',
            'max:20',
            Rule::unique('pacientes')->ignore($this->paciente),
        ],
        'nombre' => ['required', 'max:100'],
        'apellido' => ['required', 'max:100'],
    ];
}

Ese $this->paciente es el modelo que llega por la ruta.

Por eso lo habitual son dos Form Requests: GuardarPacienteRequest y ActualizarPacienteRequest. Las reglas no son idénticas.

Compartir el formulario: la tercera diferencia bien resuelta

Crear y editar comparten casi todos los campos. Repetirlos significa que cualquier cambio hay que hacerlo dos veces, y tarde o temprano se olvida uno.

La solución es un fragmento compartido:

{{-- pacientes/partials/campos.blade.php --}}
<x-campo nombre="cedula" etiqueta="Cédula"
         :valor="$paciente->cedula ?? null" requerido />
<x-campo nombre="nombre" etiqueta="Nombre"
         :valor="$paciente->nombre ?? null" requerido />
<x-campo nombre="apellido" etiqueta="Apellido"
         :valor="$paciente->apellido ?? null" requerido />
<x-campo nombre="telefono" etiqueta="Teléfono"
         :valor="$paciente->telefono ?? null" />

Ese ?? null permite que funcione también al crear, donde $paciente no existe.

Crear:

<form method="POST" action="{{ route('pacientes.store') }}">
    @csrf
    @include('pacientes.partials.campos')
    <button type="submit">Guardar</button>
</form>

Editar:

<form method="POST" action="{{ route('pacientes.update', $paciente) }}">
    @csrf
    @method('PUT')
    @include('pacientes.partials.campos')
    <button type="submit">Actualizar</button>
</form>

Un solo archivo con los campos. Añadir un campo nuevo es tocar un archivo, no dos.

PUT o PATCH

Route::resource registra ambos para la misma acción, así que funcionan igual. La diferencia es semántica:

  • PUT significa reemplazar el recurso completo.
  • PATCH significa modificar solo algunos campos.

En una aplicación web con formularios completos, usa PUT. PATCH tiene más sentido en APIs donde se envían campos sueltos.

Actualizar relaciones muchos a muchos

Cuando el formulario incluye casillas de verificación:

public function update(ActualizarCitaRequest $request, Cita $cita)
{
    $cita->update($request->validated());

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

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

sync() y no attach(). Como vimos en la lección de relaciones: sync() deja exactamente los que le pasas, añadiendo los nuevos y quitando los que se desmarcaron. attach() solo añade, y al editar dos veces terminarías con duplicados.

Y el ?? [] es necesario: si el usuario desmarca todas las casillas, el campo no llega en la petición, y sin ese valor por defecto obtendrías un error.

Saber qué cambió

A veces hay que reaccionar solo cuando un campo concreto cambió:

public function update(ActualizarCitaRequest $request, Cita $cita)
{
    $estadoAnterior = $cita->estado;

    $cita->update($request->validated());

    if ($cita->wasChanged('estado')) {
        // registrar en auditoría, notificar al paciente...
        logger("Cita {$cita->id}: {$estadoAnterior} → {$cita->estado}");
    }

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

Los métodos disponibles:

$cita->isDirty();              // ¿tiene cambios sin guardar?
$cita->isDirty('estado');
$cita->wasChanged();           // ¿cambió al guardar?
$cita->getOriginal('estado');  // el valor anterior
$cita->getChanges();           // qué cambió exactamente

Actualizaciones parciales

Para acciones puntuales que no requieren el formulario completo, conviene una ruta propia:

Route::patch('/citas/{cita}/atender', [CitaController::class, 'atender'])
    ->name('citas.atender');
public function atender(Cita $cita)
{
    $cita->update([
        'estado' => 'atendida',
        'atendida_en' => now(),
    ]);

    return back()->with('exito', 'Cita marcada como atendida');
}

Es más claro que forzar esa acción por el update() genérico. Y back() devuelve al usuario a donde estaba, que en un listado es justo lo que espera.

Edición concurrente

Un problema real en sistemas con varios usuarios: dos personas abren el mismo registro, y quien guarda último pisa los cambios del otro sin que nadie se entere.

La solución habitual es comparar la marca de tiempo:

<input type="hidden" name="updated_at" value="{{ $paciente->updated_at }}">
if ($request->updated_at != $paciente->updated_at) {
    return back()
        ->withInput()
        ->with('error', 'Otro usuario modificó este registro. Revisa los cambios antes de guardar.');
}

No hace falta en todos los proyectos, pero en sistemas donde varias personas editan lo mismo evita pérdidas de datos silenciosas.

Errores comunes

  • Olvidar @method('PUT') y recibir un error 405.
  • unique sin ignore(), y no poder guardar sin cambiar la cédula.
  • old() sin valor por defecto, dejando el formulario vacío al editar.
  • attach() en lugar de sync(), duplicando relaciones.
  • Repetir el formulario en crear y editar.
  • Olvidar ?? [] en el sync() cuando se desmarcan todas las opciones.

Para cerrar

Actualizar es crear con tres diferencias: el método PUT, la regla unique con ignore() y sync() para las relaciones. Lo demás es idéntico, y por eso conviene compartir el formulario.

En la siguiente lección cerramos el CRUD con el detalle y la eliminación.

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