Curso de Laravel
Operaciones CRUD en Laravel: ver y eliminar registros
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Cerramos el módulo del CRUD con las dos operaciones que faltan: ver el detalle de un registro y eliminarlo.
La primera es la más sencilla de las cinco. La segunda es la única que no tiene vuelta atrás, y por eso merece más cuidado del que suele recibir.
¡Empecemos!
El método show
public function show(Paciente $paciente)
{
return view('pacientes.show', compact('paciente'));
}
Una línea. La vinculación de modelos resuelve el registro y devuelve un 404 si no existe.
Si la vista muestra relaciones, cárgalas aquí:
public function show(Paciente $paciente)
{
$paciente->load([
'citas' => fn ($q) => $q->latest('fecha')->limit(10),
'citas.doctor',
]);
return view('pacientes.show', compact('paciente'));
}
load() y no with(). El modelo ya está resuelto, así que se le añaden las relaciones después. with() se usa cuando estás construyendo la consulta.
Y ese limit(10) evita el caso incómodo del paciente con doscientas citas cargadas de una sola vez.
La vista de detalle
@extends('layouts.app')
@section('titulo', $paciente->nombre)
@section('contenido')
<div class="cabecera">
<h1>{{ $paciente->nombre }} {{ $paciente->apellido }}</h1>
<div class="acciones">
<a href="{{ route('pacientes.edit', $paciente) }}" class="boton">Editar</a>
<a href="{{ route('pacientes.index') }}">Volver al listado</a>
</div>
</div>
<x-tarjeta>
<x-slot:cabecera>Datos personales</x-slot:cabecera>
<dl>
<dt>Cédula</dt>
<dd>{{ $paciente->cedula }}</dd>
<dt>Fecha de nacimiento</dt>
<dd>{{ $paciente->fecha_nacimiento->format('d/m/Y') }}</dd>
<dt>Edad</dt>
<dd>{{ $paciente->fecha_nacimiento->age }} años</dd>
<dt>Teléfono</dt>
<dd>{{ $paciente->telefono ?? 'No registrado' }}</dd>
</dl>
</x-tarjeta>
<h2>Últimas citas</h2>
<table>
@forelse ($paciente->citas as $cita)
<tr>
<td>{{ $cita->fecha->format('d/m/Y H:i') }}</td>
<td>{{ $cita->doctor->nombre }}</td>
<td>{{ $cita->estado }}</td>
</tr>
@empty
<tr><td colspan="3">Este paciente no tiene citas registradas.</td></tr>
@endforelse
</table>
@endsection
Ese ->age y ese ->format() funcionan porque el campo está declarado como fecha en el modelo:
protected function casts(): array
{
return [
'fecha_nacimiento' => 'date',
];
}
Sin esa conversión, fecha_nacimiento sería un texto y format() daría error. Es de las cosas que más tiempo hacen perder cuando no se sabe dónde mirar.
Eliminar: el formulario, no el enlace
Aquí está el error más frecuente de todo el CRUD:
{{-- MAL --}}
<a href="{{ route('pacientes.destroy', $paciente) }}">Eliminar</a>
Eso no funciona, y aunque funcionara sería peligroso.
No funciona porque la ruta espera DELETE y un enlace envía GET.
Sería peligroso porque un GET no debe modificar nada. El navegador precarga enlaces, los buscadores los recorren, y las extensiones los siguen. Ha pasado más de una vez que un rastreador vacía una base de datos entera pasando por todos los enlaces de «eliminar».
La forma correcta:
<form method="POST" action="{{ route('pacientes.destroy', $paciente) }}"
onsubmit="return confirm('¿Eliminar a {{ $paciente->nombre }}? Esta acción no se puede deshacer.')">
@csrf
@method('DELETE')
<button type="submit" class="boton-peligro">Eliminar</button>
</form>
Tres piezas: @csrf, @method('DELETE') y una confirmación.
El método destroy
public function destroy(Paciente $paciente)
{
$paciente->delete();
return redirect()
->route('pacientes.index')
->with('exito', 'Paciente eliminado');
}
Fíjate en que redirige al listado, no al detalle: el registro que se mostraba ya no existe.
Qué pasa con los registros relacionados
Si el paciente tiene citas y lo eliminas, esas citas quedan apuntando a un id que ya no existe.
Hay tres formas de resolverlo, y conviene elegir conscientemente.
Borrado en cascada en la migración:
$tabla->foreignId('paciente_id')
->constrained()
->cascadeOnDelete();
Al eliminar el paciente, la base de datos elimina sus citas. Es lo correcto cuando los hijos no tienen sentido sin el padre.
Impedir el borrado:
$tabla->foreignId('paciente_id')
->constrained()
->restrictOnDelete();
La base de datos rechaza la operación si hay citas. Y para dar un mensaje decente en lugar de un error de SQL:
public function destroy(Paciente $paciente)
{
if ($paciente->citas()->exists()) {
return back()->with('error',
'No se puede eliminar: el paciente tiene citas registradas.');
}
$paciente->delete();
return redirect()->route('pacientes.index')->with('exito', 'Paciente eliminado');
}
Nunca eliminar de verdad, que es la opción siguiente y la que uso en casi todos los sistemas.
Borrado lógico: SoftDeletes
En un sistema real, borrar datos definitivamente casi nunca es lo que quieres. Alguien elimina por error, aparece una auditoría, hace falta un histórico.
Laravel resuelve esto marcando el registro como eliminado en lugar de quitarlo.
En la migración:
$tabla->softDeletes();
Eso añade una columna deleted_at que admite nulos.
En el modelo:
use Illuminate\Database\Eloquent\SoftDeletes;
class Paciente extends Model
{
use SoftDeletes;
}
Y ya está. No cambia nada más en tu código. delete() ahora llena deleted_at, y todas las consultas ignoran automáticamente los registros marcados.
Trabajar con los eliminados
Paciente::withTrashed()->get(); // todos, incluidos los eliminados
Paciente::onlyTrashed()->get(); // solo los eliminados
$paciente->trashed(); // ¿está eliminado?
$paciente->restore(); // devolverlo
$paciente->forceDelete(); // eliminar de verdad
Una papelera de reciclaje sale casi gratis:
public function papelera()
{
$pacientes = Paciente::onlyTrashed()->latest('deleted_at')->paginate(15);
return view('pacientes.papelera', compact('pacientes'));
}
public function restaurar($id)
{
$paciente = Paciente::onlyTrashed()->findOrFail($id);
$paciente->restore();
return back()->with('exito', 'Paciente restaurado');
}
Fíjate en el findOrFail($id) en lugar de la vinculación de modelos: la vinculación automática no encuentra registros eliminados, porque aplica el mismo filtro que el resto de consultas.
Las rutas:
Route::get('/pacientes/papelera', [PacienteController::class, 'papelera'])
->name('pacientes.papelera');
Route::patch('/pacientes/{id}/restaurar', [PacienteController::class, 'restaurar'])
->name('pacientes.restaurar');
El orden importa. La ruta de la papelera debe declararse antes de Route::resource, o papelera se interpretará como el id de un paciente.
Cuidado con unique
Un detalle que sorprende: si eliminas lógicamente a un paciente con cédula 1234567 y luego intentas registrar otro con la misma cédula, la restricción unique lo impide, porque el registro sigue en la tabla.
La solución depende del caso: excluir los eliminados de la validación, o decidir que esa cédula queda reservada. Lo importante es saber que pasa.
Eliminación en lote
Cuando la interfaz permite marcar varias filas:
public function eliminarVarios(Request $request)
{
$datos = $request->validate([
'ids' => ['required', 'array'],
'ids.*' => ['exists:pacientes,id'],
]);
$eliminados = Paciente::whereIn('id', $datos['ids'])->delete();
return back()->with('exito', "Se eliminaron {$eliminados} pacientes");
}
Ese whereIn(...)->delete() es una sola consulta, no una por registro.
Pero atención: al eliminar así, no se disparan los eventos del modelo (deleting, deleted). Si dependes de ellos, hay que recorrer los registros uno por uno.
Confirmación seria para casos serios
El confirm() del navegador sirve para lo cotidiano. Cuando la operación es grave —eliminar una empresa con todos sus datos, por ejemplo— conviene una página de confirmación que obligue a escribir el nombre:
<form method="POST" action="{{ route('empresas.destroy', $empresa) }}">
@csrf
@method('DELETE')
<p>Se eliminarán también <strong>{{ $empresa->usuarios_count }} usuarios</strong>
y <strong>{{ $empresa->facturas_count }} facturas</strong>.</p>
<label>Escribe <code>{{ $empresa->nombre }}</code> para confirmar:</label>
<input type="text" name="confirmacion" required>
<button type="submit">Eliminar definitivamente</button>
</form>
if ($request->confirmacion !== $empresa->nombre) {
return back()->with('error', 'El nombre no coincide.');
}
Es fricción a propósito, y en esos casos es exactamente lo que hace falta.
El CRUD completo
Con esta lección cerramos las cinco operaciones y las siete rutas que genera Route::resource:
| Método | URL | Acción | Qué hace |
|---|---|---|---|
| GET | /pacientes |
index | Listar |
| GET | /pacientes/create |
create | Formulario de creación |
| POST | /pacientes |
store | Guardar |
| GET | /pacientes/{id} |
show | Ver detalle |
| GET | /pacientes/{id}/edit |
edit | Formulario de edición |
| PUT | /pacientes/{id} |
update | Actualizar |
| DELETE | /pacientes/{id} |
destroy | Eliminar |
Esto es la base de cualquier sistema de gestión. Todo lo demás son variaciones sobre estas siete acciones.
Errores comunes
- Un enlace para eliminar en lugar de un formulario con DELETE.
- Olvidar
@method('DELETE')y recibir un 405. - Eliminar sin confirmación.
- No pensar en los registros relacionados hasta que salta el error de clave foránea.
- Borrar de verdad lo que debería marcarse como eliminado.
- Usar vinculación de modelos para restaurar registros eliminados lógicamente.
load()ywith()confundidos.
Para cerrar
Ver un registro es la operación más simple del CRUD; eliminarlo es la que más consecuencias tiene.
Si te quedas con dos cosas: eliminar siempre por formulario con DELETE, y SoftDeletes por defecto en todo lo que le importe al negocio. La columna deleted_at cuesta nada y algún día te va a salvar.
En la siguiente lección pondremos en español todos los mensajes del framework.
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.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp