6.3 Desarrollo de un CRUD Completo en Laravel (Parte 1)

6.3.1 Introducción a CRUD

El término CRUD corresponde a las operaciones básicas que se realizan en la mayoría de las aplicaciones que gestionan datos:

  • Create (Crear): Insertar nuevos datos.
  • Read (Leer): Consultar y visualizar datos.
  • Update (Actualizar): Modificar datos existentes.
  • Delete (Eliminar): Borrar datos.

En Laravel, la implementación de un CRUD completo nos permite comprender cómo los Modelos, Controladores y Vistas interactúan entre sí para ofrecer una experiencia de usuario completa.

Importante

Dominar el desarrollo de CRUDs es básico para cualquier programador web, ya que casi todas las aplicaciones web tienen que manejar datos de algún tipo.

6.3.2 Rutas Dinámicas y Controladores

6.3.2.1 Parámetros Dinámicos en Rutas

En Laravel, podemos definir rutas que aceptan parámetros dinámicos. Estos parámetros permiten que una misma ruta atienda solicitudes distintas según el valor proporcionado.

Ejemplo sencillo:

Route::get('/nota/{id}', function ($id) {
    return "Mostrando la nota con ID: $id";
});

En este caso, id es un parámetro dinámico. Si accedemos a http://localhost:8080/nota/5, Laravel mostrará: "Mostrando la nota con ID: 5".

Múltiples parámetros:

Route::get('/usuario/{id}/nota/{nota_id}', function ($id, $nota_id) {
    return "Usuario $id - Nota $nota_id";
});

Importante

El orden de los parámetros en la URL debe coincidir exactamente con el orden de los parámetros en la función anónima o el controlador.

6.3.2.2 Crear un Circuito MVC Rápido para Rutas Dinámicas

Modelo: Vamos a utilizar una tabla notes con los campos:

  • id (entero, autoincremental)
  • title (string)
  • description (text)
  • date (date)
  • done (boolean)

Vamos a crear la migración de la nota y el controador:

php artisan make:migration create_notes_table
php artisan make:model Note

Podemos crearla migración y la nota con un solo comando:

php artisan make:model Note -m

Ahora vamos al archivo de migración database/migrations/xxxx_xx_xx_create_notes_table.php y añadimos los campos:

Migración

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public function up()
{
    Schema::create('notes', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->date('date');
        $table->boolean('done')->default(false);
        $table->timestamps();
    });
}
public function down()
{
    Schema::dropIfExists('notes');
}

Por último, ejecutamos la migración:

php artisan migrate

Si tenemos algún problema porque no hemos creado la base de datos desde 0, podemos eliminar las migraciones anteiores con:

php artisan migrate:reset

Modelo Note:

Modelo

1
2
3
4
5
6
7
8
9
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Note extends Model
{
    protected $fillable = ['title', 'description', 'date', 'done'];
    protected $guarded = ['id'];
}
  • $fillable: Define qué campos se pueden asignar en masa.
  • $guarded: Define qué campos no se pueden asignar.

Controlador NoteController:

php artisan make:controller NoteController

Método para mostrar un ID:

1
2
3
4
public function show($id)
{
    return view('notes.show', compact('id'));
}

Ruta asociada:

Route::get('/note/{id}', [NoteController::class, 'show'])->name('note.show');

Vista resources/views/notes/show.blade.php:

<h1>Detalle de Nota</h1>
<p>El ID de la nota es: {{ $id }}</p>

Con esto, accediendo a /note/5 veremos "El ID de la nota es: 5".

Compact

La función compact('variable') crea un array asociativo ['variable' => $variable] que puede ser pasado a la vista. Es una forma rápida y limpia de pasar datos.

6.3.2.3 Parámetros Opcionales y Valores por Defecto

Podemos definir parámetros opcionales añadiendo un signo de interrogación ?:

1
2
3
Route::get('/saludo/{nombre?}', function ($nombre = 'Invitado') {
    return "Hola, $nombre";
});
  • Si accedemos a /saludo/Laura, veremos "Hola, Laura".
  • Si accedemos a /saludo, veremos "Hola, Invitado".

Notas importantes: - El parámetro opcional debe ser el último de la URL. - Hay que asignar un valor por defecto en la función.

6.3.2.4 Importancia del Orden de las Rutas

Laravel evalúa las rutas en el orden en que se definen.

Ejemplo de conflicto:

Route::get('/nota/nueva', function() { return 'Crear nueva nota'; });
Route::get('/nota/{id}', function($id) { return "Nota ID: $id"; });
  • Primero debe definirse /nota/nueva porque si no, Laravel intentará interpretar nueva como un id.
  • El orden correcto es siempre de rutas más específicas a más generales.

Consejo

Primero define todas las rutas fijas y luego las rutas con parámetros dinámicos.

6.3.3 Desarrollo del CRUD para Notas

6.3.3.2 Listar Todas las Notas

Primero creamos la ruta y el método para listar todas las notas.

Ruta:

Route::get('/', [NoteController::class, 'index'])->name('note.index');

Controlador:

1
2
3
4
5
public function index()
{
    $notes = Note::all();
    return view('notes.index', compact('notes'));
}

Explicación de @forelse vs @foreach:

  • @foreach se utiliza para recorrer elementos, pero no gestiona si el array está vacío.
  • @forelse permite recorrer elementos y además definir qué hacer si no hay elementos.

Ejemplo:

Listado de Notas
1
2
3
4
5
@forelse ($notes as $note)
    <p>{{ $note->title }}</p>
@empty
    <p>No hay notas disponibles.</p>
@endforelse

Crear layout base en resources/views/layouts/app.blade.php:

Layout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>@yield('title')</title>
</head>
<body>
    <header>
        <h1>Mi Aplicación de Notas</h1>
        <nav>
            <a href="{{ route('note.index') }}">Inicio</a> |
            <a href="{{ route('note.create') }}">Crear Nota</a>
        </nav>
    </header>
    <main>
        @yield('content')
    </main>
</body>
</html>

Vista de Listado resources/views/notes/index.blade.php:

Listado de Notas

@extends('layouts.app')

@section('title', 'Listado de Notas')

@section('content')
    <h2>Listado de Notas</h2>

    @forelse ($notes as $note)
        <div>
            <h3>{{ $note->title }}</h3>
            <p>{{ $note->description }}</p>
            <small>{{ $note->date }}</small>
            <div>
                <a href="{{ route('note.edit', $note->id) }}">Editar</a>
                <form action="{{ route('note.destroy', $note->id) }}" method="POST" style="display:inline">
                    @csrf
                    @method('DELETE')
                    <button type="submit">Eliminar</button>
                </form>
            </div>
        </div>
    @empty
        <p>No hay notas disponibles.</p>
    @endforelse
@endsection

6.3.3.3 Crear una Nueva Nota

Ruta para formulario de creación:

Route::get('/note/create', [NoteController::class, 'create'])->name('note.create');

Controlador:

1
2
3
4
public function create()
{
    return view('notes.create');
}

Vista resources/views/notes/create.blade.php:

Formulario de Crear Nota

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@extends('layouts.app')

@section('title', 'Crear Nueva Nota')

@section('content')
    <h2>Crear Nota</h2>
    <form action="{{ route('note.store') }}" method="POST">
        @csrf
        <label>Título:</label>
        <input type="text" name="title" required>

        <label>Descripción:</label>
        <textarea name="description" required></textarea>

        <label>Fecha:</label>
        <input type="date" name="date" required>

        <label>Completada:</label>
        <input type="checkbox" name="done">

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

6.3.3.4 Guardar la Nueva Nota

Ruta para guardar:

Route::post('/note/store', [NoteController::class, 'store'])->name('note.store');

Controlador:

Para guardar la nota, podemos usar diferentes métodos. Aquí mostramos dos formas:

Guardar Nota

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public function store(Request $request)
{
    note = new Note();
    $note->title = $request->input('title');
    $note->description = $request->input('description');
    $note->date = $request->input('date');
    $note->done = $request->input('done') ? 1 : 0;
    $note->save();
    // Redirigir a la lista de notas
    return redirect()->route('note.index');
}

O usando el método create:

1
2
3
4
5
public function store(Request $request)
{
    Note::create($request->all());
    return redirect()->route('note.index');
}

Explicaciones Adicionales:

  • @csrf protege contra ataques CSRF (Cross-Site Request Forgery).
  • $request->all() devuelve todos los datos enviados en el formulario.
  • Laravel valida automáticamente que el token CSRF esté presente. Si no lo está, lanzará un error.

¿Cómo funciona CSRF? - Laravel genera un token único para cada sesión de usuario. - Este token se incluye en cada formulario generado por Laravel. - Cuando se envía el formulario, Laravel verifica que el token enviado coincida con el de la sesión. - Si no coinciden, Laravel lanza un error 419 (Page Expired). - Esto previene que un atacante envíe formularios en nombre del usuario sin su consentimiento.


6.3.3.5 Editar una Nota

Ruta para formulario de edición:

Route::get('/note/edit/{note}', [NoteController::class, 'edit'])->name('note.edit');

Controlador:

Tenemos varias formas de recibir el parámetro note. En esta primer recibimos el ID y buscamos la nota, para poder pasarla a la vista:

1
2
3
4
5
public function edit($id)
{
    $note = Note::findOrFail($id);
    return view('notes.edit', compact('note'));
}
En esta segunda forma, recibimos el modelo Note. De esta manera es Laravel el que se encarga de buscar la nota:

1
2
3
4
public function edit(Note $note)
{
    return view('notes.edit', compact('note'));
}

Vista resources/views/notes/edit.blade.php:

En este caso la ruta la hemos definido con el método PUT. Este método es el que se utiliza para actualizar los datos de un recurso existente. Pero ¿cómo hacerlo si las opciones de form solo permiten GET y POST?. Laravel nos ofrece una solución sencilla: la directiva @method('PUT'). Esta directiva simula el método PUT en formularios HTML. Esta directiva debe estar dentro del formulario y antes de los inputs.

Editar Nota
1
2
3
4
5
<form id="sample-form" action="somepage.php" method="POST">
    @csrf
    @method('PUT')
    <!-- Otros campos del formulario -->
</form>

Con este formato el formulario se enviará como un PUT, aunque el método del formulario sea POST.

Formulario de Editar Nota

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@extends('layouts.app')

@section('title', 'Editar Nota')

@section('content')
    <h2>Editar Nota</h2>
    <form action="{{ route('note.update', $note->id) }}" method="POST">
        @csrf
        @method('PUT')
        <label>Título:</label>
        <input type="text" name="title" value="{{ $note->title }}" required>

        <label>Descripción:</label>
        <textarea name="description" required>{{ $note->description }}</textarea>

        <label>Fecha:</label>
        <input type="date" name="date" value="{{ $note->date }}" required>

        <label>Completada:</label>
        <input type="checkbox" name="done" {{ $note->done ? 'checked' : '' }}>

        <button type="submit">Actualizar</button>
        <a href="{{ route('note.index') }}">Cancelar</a>
    </form>
@endsection

6.3.3.6 Actualizar la Nota

Ruta para actualizar:

Route::put('/note/update/{note}', [NoteController::class, 'update'])->name('note.update');

Controlador:

1
2
3
4
5
public function update(Request $request, Note $note)
{
    $note->update($request->all());
    return redirect()->route('note.index');
}
  • @method('PUT') simula el método HTTP PUT en formularios HTML (que solo permiten GET y POST).

6.3.3.7 Mostrar una Nota Individual

Ruta para mostrar:

Route::get('/note/show/{note}', [NoteController::class, 'show'])->name('note.show');

Controlador:

1
2
3
4
public function show(Note $note)
{
    return view('notes.show', compact('note'));
}

Vista resources/views/notes/show.blade.php actualizada:

Mostrar Nota

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@extends('layouts.app')

@section('title', 'Detalle de Nota')

@section('content')
    <h2>{{ $note->title }}</h2>
    <p>{{ $note->description }}</p>
    <p>Fecha: {{ $note->date }}</p>
    <p>Estado: {{ $note->done ? 'Completada' : 'Pendiente' }}</p>
    <a href="{{ route('note.index') }}">Volver</a>
@endsection

6.3.3.8 Eliminar una Nota

Ruta para eliminar:

Route::delete('/note/destroy/{note}', [NoteController::class, 'destroy'])->name('note.destroy');

Controlador:

1
2
3
4
5
public function destroy(Note $note)
{
    $note->delete();
    return redirect()->route('note.index');
}
  • El método delete() elimina el registro de la base de datos.

6.3.3.9 Tipado en los Métodos del Controlador

Ejemplo de tipado correcto:

1
2
3
4
5
public function update(Request $request, Note $note): RedirectResponse
{
    $note->update($request->all());
    return redirect()->route('note.index');
}
  • Tipar los parámetros mejora la legibilidad y control de errores.
  • Tipar el tipo de retorno ayuda a Laravel a validar internamente las respuestas.

Tipos comunes de retorno

  • View para devolver vistas.
  • RedirectResponse para redirecciones.
  • JsonResponse para APIs.

Ejercicio para el Tema 8: CRUD de Productos

Enunciado

Objetivo: Crear un CRUD completo para gestionar productos en tu aplicación Laravel.

La tabla de productos debe tener los siguientes campos:

  • id (auto-incremental)
  • name (string 255)
  • description (text)
  • price (decimal 8,2)
  • stock (integer)
  • timestamps

Pasos a seguir:

  • Crea la migración para la tabla products (si no existe)
  • Crea el modelo Product si no lo tienes ya, asegurándote de definir $fillable.
  • Crea el controlador ProductController como resource:
  • Define la rutas usando resource:
  • Crea las vistas en resources/views/products/ para:

    • index.blade.php ➔ Listar productos
    • create.blade.php ➔ Formulario de nuevo producto
    • edit.blade.php ➔ Formulario para editar
    • show.blade.php ➔ Mostrar detalles del producto
  • Enlaza todas las acciones desde el listado (index).

  • Usa layouts y secciones (@section('title'), @section('content')).

Ejercicio Tema: Soluiones

Solución Tema 8

Ver Solución Tema 8
// routes/web.php
use App\Http\Controllers\ProductController;
Route::resource('product', ProductController::class);
// Crear el controlador
php artisan make:controller ProductController --resource
// Crear el modelo y la migración si no existe
php artisan make:model Product -m
// database/migrations/xxxx_create_products_table.php
public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name', 255);
        $table->text('description');
        $table->decimal('price', 8, 2);
        $table->integer('stock');
        $table->timestamps();
    });
}
// app/Models/Product.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['name', 'description', 'price', 'stock'];
}
<!-- resources/views/products/index.blade.php -->
@extends('layouts.app')

@section('title', 'Listado de Productos')

@section('content')
    <h2>Productos</h2>
    <a href="{{ route('product.create') }}">Crear Producto</a>
    @forelse ($products as $product)
        <div>
            <h3>{{ $product->name }}</h3>
            <p>{{ $product->description }}</p>
            <p>Precio: ${{ $product->price }}</p>
            <p>Stock: {{ $product->stock }}</p>
            <a href="{{ route('product.edit', $product) }}">Editar</a>
            <form action="{{ route('product.destroy', $product) }}" method="POST" style="display:inline;">
                @csrf
                @method('DELETE')
                <button type="submit">Eliminar</button>
            </form>
        </div>
    @empty
        <p>No hay productos disponibles.</p>
    @endforelse
@endsection