CRUD Completo con Laravel 12 - Guía Práctica Paso a Paso-productos

 Introducción

En este tutorial, construiremos una aplicación CRUD (Create, Read, Update, Delete) completa desde cero utilizando Laravel 12. Desarrollaremos un sistema de gestión de productos que te permitirá realizar todas las operaciones básicas de base de datos. Este tutorial es ideal para consolidar todos los conocimientos adquiridos en los temas anteriores.

Índice

  1. Configuración del Proyecto

  2. Creación de la Migración

  3. Creación del Seeder

  4. Creación del Modelo

  5. Configuración del Enrutador

  6. Creación del Controlador

  7. Creación de las Vistas

  8. Ejecución y Pruebas

  9. Mejoras y Optimizaciones

  10. Ejercicio de Consolidación


1. Configuración del Proyecto

Crear nuevo proyecto Laravel

bash
# Crear el proyecto
composer create-project laravel/laravel crud-productos

# Navegar al directorio
cd crud-productos

# Configurar archivo .env
# Editar el archivo .env con tus credenciales de base de datos

Configuración de la base de datos

env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=crud_productos
DB_USERNAME=root
DB_PASSWORD=

Crear la base de datos

sql
-- Crear la base de datos en MySQL
CREATE DATABASE crud_productos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2. Creación de la Migración

Generar la migración

bash
php artisan make:migration create_products_table

Código de la migración

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name', 100);
            $table->string('description', 500);
            $table->decimal('price', 8, 2);
            $table->integer('stock')->default(0);
            $table->boolean('available')->default(true);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Ejecutar la migración

bash
php artisan migrate

3. Creación del Seeder

Generar el seeder

bash
php artisan make:seeder ProductTableSeeder

Código del seeder

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

class ProductTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Datos de ejemplo en español
        $products = [
            [
                'name' => 'Papel Higiénico Extrasuave',
                'description' => 'Papel higiénico de triple capa, suave y resistente, ideal para pieles sensibles.',
                'price' => 1.35,
                'stock' => 100,
                'available' => true,
            ],
            [
                'name' => 'Lejía Blanca 1L',
                'description' => 'Lejía pura de alta concentración, perfecta para desinfectar y blanquear superficies.',
                'price' => 0.95,
                'stock' => 50,
                'available' => true,
            ],
            [
                'name' => 'Detergente Líquido Ariel 1.5L',
                'description' => 'Detergente líquido para lavadoras de alta eficiencia, elimina manchas difíciles.',
                'price' => 4.90,
                'stock' => 30,
                'available' => true,
            ],
            [
                'name' => 'Gel Hidroalcohólico 350ml',
                'description' => 'Gel hidroalcohólico con 70% de alcohol, sin perfume, ideal para desinfección.',
                'price' => 3.50,
                'stock' => 75,
                'available' => true,
            ],
            [
                'name' => 'Gel de Baño con Avena 750ml',
                'description' => 'Gel de baño hipoalergénico con aceites vegetales y extracto de avena.',
                'price' => 1.50,
                'stock' => 60,
                'available' => true,
            ],
            [
                'name' => 'Aceite de Oliva Virgen Extra 1L',
                'description' => 'Aceite de oliva virgen extra de primera presión en frío, origen español.',
                'price' => 6.95,
                'stock' => 25,
                'available' => true,
            ],
            [
                'name' => 'Arroz Integral 1kg',
                'description' => 'Arroz integral de grano largo, rico en fibra y nutrientes.',
                'price' => 2.30,
                'stock' => 40,
                'available' => true,
            ],
            [
                'name' => 'Lentejas Rojas 500g',
                'description' => 'Lentejas rojas de cocción rápida, ricas en proteínas y hierro.',
                'price' => 1.20,
                'stock' => 35,
                'available' => true,
            ],
        ];

        // Insertar los productos
        foreach ($products as $product) {
            DB::table('products')->insert([
                'name' => $product['name'],
                'description' => $product['description'],
                'price' => $product['price'],
                'stock' => $product['stock'],
                'available' => $product['available'],
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }
}

Registrar el seeder en DatabaseSeeder

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            ProductTableSeeder::class,
        ]);
    }
}

Ejecutar el seeder

bash
# Ejecutar migración y seeders juntos
php artisan migrate:fresh --seed

# O ejecutar solo el seeder
php artisan db:seed --class=ProductTableSeeder

4. Creación del Modelo

Generar el modelo

bash
php artisan make:model Product

Código del modelo

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    /**
     * The table associated with the model.
     */
    protected $table = 'products';

    /**
     * The attributes that are mass assignable.
     */
    protected $fillable = [
        'name',
        'description',
        'price',
        'stock',
        'available',
    ];

    /**
     * The attributes that should be cast.
     */
    protected $casts = [
        'price' => 'float',
        'stock' => 'integer',
        'available' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    /**
     * Get the formatted price.
     */
    public function getFormattedPriceAttribute(): string
    {
        return '$' . number_format($this->price, 2);
    }

    /**
     * Scope for available products.
     */
    public function scopeAvailable($query)
    {
        return $query->where('available', true);
    }

    /**
     * Scope for products with stock.
     */
    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }
}

5. Configuración del Enrutador

Rutas REST (Resource)

php
// routes/web.php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

// Ruta resource completa (genera las 7 rutas REST)
Route::resource('products', ProductController::class);

// Alternativa: Definir las rutas manualmente
/*
Route::get('product', [ProductController::class, 'index'])->name('product.index');
Route::get('product/{product}', [ProductController::class, 'show'])->name('product.show');
Route::get('product/create', [ProductController::class, 'create'])->name('product.create');
Route::post('product', [ProductController::class, 'store'])->name('product.store');
Route::get('product/{product}/edit', [ProductController::class, 'edit'])->name('product.edit');
Route::put('product/{product}', [ProductController::class, 'update'])->name('product.update');
Route::delete('product/{product}', [ProductController::class, 'destroy'])->name('product.destroy');
*/

// Ruta para la página de inicio
Route::get('/', function () {
    return redirect()->route('products.index');
});

Verificar las rutas

bash
php artisan route:list
MétodoURINombreAcción
GET/Closure
GETproductsproducts.indexindex
GETproducts/createproducts.createcreate
POSTproductsproducts.storestore
GETproducts/{product}products.showshow
GETproducts/{product}/editproducts.editedit
PUT/PATCHproducts/{product}products.updateupdate
DELETEproducts/{product}products.destroydestroy

6. Creación del Controlador

Generar el controlador con resource

bash
php artisan make:controller ProductController --resource --model=Product

Código del controlador

php
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;

class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): View
    {
        $productList = Product::all();
        return view('products.index', compact('productList'));
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create(): View
    {
        return view('products.form');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request): RedirectResponse
    {
        // Validación de datos
        $validated = $request->validate([
            'name' => 'required|string|max:100',
            'description' => 'required|string|max:500',
            'price' => 'required|numeric|min:0|max:999999.99',
            'stock' => 'nullable|integer|min:0',
            'available' => 'nullable|boolean',
        ]);

        // Crear producto con asignación masiva
        $product = new Product($validated);
        $product->available = $request->has('available');
        $product->save();

        // Redireccionar con mensaje de éxito
        return redirect()
            ->route('products.index')
            ->with('success', 'Producto creado exitosamente.');
    }

    /**
     * Display the specified resource.
     */
    public function show(Product $product): View
    {
        return view('products.show', compact('product'));
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Product $product): View
    {
        return view('products.form', compact('product'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Product $product): RedirectResponse
    {
        // Validación de datos
        $validated = $request->validate([
            'name' => 'required|string|max:100',
            'description' => 'required|string|max:500',
            'price' => 'required|numeric|min:0|max:999999.99',
            'stock' => 'nullable|integer|min:0',
            'available' => 'nullable|boolean',
        ]);

        // Actualizar producto
        $product->update($validated);
        $product->available = $request->has('available');
        $product->save();

        // Redireccionar con mensaje de éxito
        return redirect()
            ->route('products.index')
            ->with('success', 'Producto actualizado exitosamente.');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Product $product): RedirectResponse
    {
        $product->delete();

        return redirect()
            ->route('products.index')
            ->with('success', 'Producto eliminado exitosamente.');
    }
}

7. Creación de las Vistas

Estructura de las vistas

text
resources/views/
├── layouts/
│   └── master.blade.php
└── products/
    ├── index.blade.php
    ├── show.blade.php
    └── form.blade.php

Layout principal (layouts/master.blade.php)

blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>@yield('title', 'Gestión de Productos')</title>

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

    @stack('styles')
</head>
<body>
    <!-- Barra de navegación -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{{ route('products.index') }}">
                <i class="fas fa-store"></i> Mi Tienda
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('products.index') }}">
                            <i class="fas fa-box"></i> Productos
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('products.create') }}">
                            <i class="fas fa-plus-circle"></i> Nuevo Producto
                        </a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <!-- Mensajes flash -->
    @if(session('success'))
        <div class="container mt-3">
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                <i class="fas fa-check-circle"></i> {{ session('success') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        </div>
    @endif

    @if(session('error'))
        <div class="container mt-3">
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                <i class="fas fa-exclamation-circle"></i> {{ session('error') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        </div>
    @endif

    <!-- Contenido principal -->
    <main class="py-4">
        <div class="container">
            @yield('content')
        </div>
    </main>

    <!-- Footer -->
    <footer class="bg-dark text-white py-3 mt-5">
        <div class="container text-center">
            <small>&copy; {{ date('Y') }} Mi Tienda - CRUD con Laravel</small>
        </div>
    </footer>

    <!-- Scripts -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

    @stack('scripts')
</body>
</html>

Vista: Lista de productos (products/index.blade.php)

blade
@extends('layouts.master')

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

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1><i class="fas fa-box"></i> Lista de Productos</h1>
    <a href="{{ route('products.create') }}" class="btn btn-primary">
        <i class="fas fa-plus"></i> Nuevo Producto
    </a>
</div>

@if($productList->isEmpty())
    <div class="alert alert-info text-center">
        <i class="fas fa-info-circle"></i> No hay productos registrados.
        <a href="{{ route('products.create') }}" class="alert-link">Crear el primer producto</a>
    </div>
@else
    <div class="table-responsive">
        <table class="table table-striped table-hover">
            <thead class="table-dark">
                <tr>
                    <th>ID</th>
                    <th>Nombre</th>
                    <th>Descripción</th>
                    <th>Precio</th>
                    <th>Stock</th>
                    <th>Disponible</th>
                    <th>Acciones</th>
                </tr>
            </thead>
            <tbody>
                @foreach($productList as $product)
                    <tr>
                        <td>{{ $product->id }}</td>
                        <td>
                            <a href="{{ route('products.show', $product) }}" class="text-decoration-none">
                                <i class="fas fa-eye text-info"></i>
                                {{ $product->name }}
                            </a>
                        </td>
                        <td>{{ Str::limit($product->description, 50) }}</td>
                        <td>{{ $product->formatted_price }}</td>
                        <td>
                            @if($product->stock > 0)
                                <span class="badge bg-success">{{ $product->stock }}</span>
                            @else
                                <span class="badge bg-danger">Sin stock</span>
                            @endif
                        </td>
                        <td>
                            @if($product->available)
                                <span class="badge bg-success">
                                    <i class="fas fa-check"></i> Sí
                                </span>
                            @else
                                <span class="badge bg-danger">
                                    <i class="fas fa-times"></i> No
                                </span>
                            @endif
                        </td>
                        <td>
                            <div class="btn-group" role="group">
                                <a href="{{ route('products.show', $product) }}" class="btn btn-sm btn-info" title="Ver">
                                    <i class="fas fa-eye"></i>
                                </a>
                                <a href="{{ route('products.edit', $product) }}" class="btn btn-sm btn-warning" title="Editar">
                                    <i class="fas fa-edit"></i>
                                </a>
                                <form action="{{ route('products.destroy', $product) }}" method="POST" class="d-inline">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="btn btn-sm btn-danger" 
                                            onclick="return confirm('¿Estás seguro de eliminar este producto?')" 
                                            title="Eliminar">
                                        <i class="fas fa-trash"></i>
                                    </button>
                                </form>
                            </div>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
@endif
@endsection

Vista: Detalle de producto (products/show.blade.php)

blade
@extends('layouts.master')

@section('title', 'Detalle del Producto')

@section('content')
<div class="card">
    <div class="card-header d-flex justify-content-between align-items-center">
        <h5 class="mb-0"><i class="fas fa-box"></i> Detalle del Producto</h5>
        <div>
            <a href="{{ route('products.edit', $product) }}" class="btn btn-warning btn-sm">
                <i class="fas fa-edit"></i> Editar
            </a>
            <a href="{{ route('products.index') }}" class="btn btn-secondary btn-sm">
                <i class="fas fa-arrow-left"></i> Volver
            </a>
        </div>
    </div>
    <div class="card-body">
        <div class="row">
            <div class="col-md-6">
                <table class="table table-bordered">
                    <tr>
                        <th>ID</th>
                        <td>{{ $product->id }}</td>
                    </tr>
                    <tr>
                        <th>Nombre</th>
                        <td>{{ $product->name }}</td>
                    </tr>
                    <tr>
                        <th>Descripción</th>
                        <td>{{ $product->description }}</td>
                    </tr>
                    <tr>
                        <th>Precio</th>
                        <td><span class="h5 text-primary">{{ $product->formatted_price }}</span></td>
                    </tr>
                    <tr>
                        <th>Stock</th>
                        <td>
                            @if($product->stock > 0)
                                <span class="badge bg-success">{{ $product->stock }} unidades</span>
                            @else
                                <span class="badge bg-danger">Sin stock</span>
                            @endif
                        </td>
                    </tr>
                    <tr>
                        <th>Disponible</th>
                        <td>
                            @if($product->available)
                                <span class="badge bg-success">Disponible</span>
                            @else
                                <span class="badge bg-danger">No disponible</span>
                            @endif
                        </td>
                    </tr>
                    <tr>
                        <th>Creado</th>
                        <td>{{ $product->created_at->format('d/m/Y H:i') }}</td>
                    </tr>
                    <tr>
                        <th>Actualizado</th>
                        <td>{{ $product->updated_at->format('d/m/Y H:i') }}</td>
                    </tr>
                </table>
            </div>
        </div>
    </div>
</div>
@endsection

Vista: Formulario Crear/Editar (products/form.blade.php)

blade
@extends('layouts.master')

@section('title', isset($product) ? 'Editar Producto' : 'Nuevo Producto')

@section('content')
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">
                <h5 class="mb-0">
                    <i class="fas {{ isset($product) ? 'fa-edit' : 'fa-plus-circle' }}"></i>
                    {{ isset($product) ? 'Editar Producto' : 'Nuevo Producto' }}
                </h5>
            </div>
            <div class="card-body">
                @if(isset($product))
                    <form action="{{ route('products.update', $product) }}" method="POST">
                        @method('PUT')
                @else
                    <form action="{{ route('products.store') }}" method="POST">
                @endif
                    @csrf

                    <!-- Nombre -->
                    <div class="mb-3">
                        <label for="name" class="form-label">Nombre del Producto *</label>
                        <input type="text" 
                               class="form-control @error('name') is-invalid @enderror" 
                               id="name" 
                               name="name" 
                               value="{{ old('name', $product->name ?? '') }}"
                               placeholder="Ingresa el nombre del producto"
                               required>
                        @error('name')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>

                    <!-- Descripción -->
                    <div class="mb-3">
                        <label for="description" class="form-label">Descripción *</label>
                        <textarea class="form-control @error('description') is-invalid @enderror" 
                                  id="description" 
                                  name="description" 
                                  rows="3"
                                  placeholder="Describe el producto"
                                  required>{{ old('description', $product->description ?? '') }}</textarea>
                        @error('description')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>

                    <!-- Precio -->
                    <div class="mb-3">
                        <label for="price" class="form-label">Precio *</label>
                        <div class="input-group">
                            <span class="input-group-text">$</span>
                            <input type="number" 
                                   class="form-control @error('price') is-invalid @enderror" 
                                   id="price" 
                                   name="price" 
                                   value="{{ old('price', $product->price ?? '') }}"
                                   step="0.01"
                                   min="0"
                                   placeholder="0.00"
                                   required>
                        </div>
                        @error('price')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>

                    <!-- Stock -->
                    <div class="mb-3">
                        <label for="stock" class="form-label">Stock</label>
                        <input type="number" 
                               class="form-control @error('stock') is-invalid @enderror" 
                               id="stock" 
                               name="stock" 
                               value="{{ old('stock', $product->stock ?? 0) }}"
                               min="0"
                               placeholder="0">
                        @error('stock')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>

                    <!-- Disponible -->
                    <div class="mb-3 form-check">
                        <input type="checkbox" 
                               class="form-check-input @error('available') is-invalid @enderror" 
                               id="available" 
                               name="available" 
                               value="1"
                               {{ old('available', $product->available ?? false) ? 'checked' : '' }}>
                        <label class="form-check-label" for="available">
                            Producto disponible para la venta
                        </label>
                        @error('available')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>

                    <!-- Botones -->
                    <div class="d-grid gap-2">
                        <button type="submit" class="btn btn-primary">
                            <i class="fas fa-save"></i> 
                            {{ isset($product) ? 'Actualizar Producto' : 'Guardar Producto' }}
                        </button>
                        <a href="{{ route('products.index') }}" class="btn btn-secondary">
                            <i class="fas fa-arrow-left"></i> Cancelar
                        </a>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection

8. Ejecución y Pruebas

Levantar el servidor de desarrollo

bash
php artisan serve

Probar la aplicación

Accede a las siguientes URLs para probar el CRUD:

text
# Lista de productos
http://localhost:8000/products

# Crear nuevo producto
http://localhost:8000/products/create

# Ver detalle de producto (reemplazar 1 con ID válido)
http://localhost:8000/products/1

# Editar producto
http://localhost:8000/products/1/edit

Comandos de prueba

bash
# Verificar rutas
php artisan route:list

# Verificar migraciones
php artisan migrate:status

# Verificar datos en la base de datos
php artisan tinker
>>> App\Models\Product::all();
>>> App\Models\Product::count();
>>> exit;

# Limpiar caché si hay problemas
php artisan cache:clear
php artisan config:clear
php artisan view:clear
php artisan route:clear

9. Mejoras y Optimizaciones

1. Agregar validación personalizada

php
// En el controlador
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:100|unique:products,name',
        'price' => 'required|numeric|min:0.01',
        // ... más reglas
    ], [
        'name.required' => 'El nombre del producto es obligatorio.',
        'name.unique' => 'Ya existe un producto con ese nombre.',
        'price.min' => 'El precio debe ser mayor a 0.',
    ]);
    // ...
}

2. Agregar paginación

php
// En el controlador index()
public function index(): View
{
    $productList = Product::paginate(10);
    return view('products.index', compact('productList'));
}

// En la vista
{{ $productList->links() }}

3. Agregar búsqueda

php
// En el modelo
public function scopeSearch($query, $term)
{
    return $query->where('name', 'LIKE', "%{$term}%")
                 ->orWhere('description', 'LIKE', "%{$term}%");
}

// En el controlador
public function index(Request $request): View
{
    $query = Product::query();
    
    if ($request->has('search')) {
        $query->search($request->search);
    }
    
    $productList = $query->paginate(10);
    return view('products.index', compact('productList'));
}

4. Agregar filtros

php
// En el controlador
public function index(Request $request): View
{
    $query = Product::query();
    
    if ($request->has('available')) {
        $query->where('available', $request->available === '1');
    }
    
    if ($request->has('min_price')) {
        $query->where('price', '>=', $request->min_price);
    }
    
    if ($request->has('max_price')) {
        $query->where('price', '<=', $request->max_price);
    }
    
    $productList = $query->paginate(10);
    return view('products.index', compact('productList'));
}

5. Usar Form Requests

bash
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest
php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:100|unique:products',
            'description' => 'required|string|max:500',
            'price' => 'required|numeric|min:0.01',
            'stock' => 'nullable|integer|min:0',
            'available' => 'nullable|boolean',
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'El nombre es obligatorio.',
            'price.min' => 'El precio debe ser mayor a 0.',
        ];
    }
}

10. Ejercicio de Consolidación

Ejercicio 1: Agregar Categorías

  1. Crear migración para tabla categories

  2. Agregar relación en el modelo Product

  3. Modificar el formulario para incluir categorías

  4. Mostrar categoría en la lista

bash
php artisan make:migration create_categories_table
php artisan make:model Category -m
php artisan make:seeder CategoryTableSeeder

Ejercicio 2: Agregar Imágenes

  1. Agregar campo image_path a la migración de products

  2. Agregar campo en el formulario (file input)

  3. Implementar subida de archivos

  4. Mostrar imágenes en la vista

Ejercicio 3: Exportar a CSV

  1. Crear método en el controlador para exportar

  2. Generar archivo CSV con datos

  3. Agregar botón de exportación

Ejercicio 4: Dashboard de Estadísticas

  1. Crear vista dashboard

  2. Mostrar estadísticas básicas (total productos, promedio precios, etc.)

  3. Crear gráficos simples con Chart.js


Resumen del CRUD

OperaciónMétodo HTTPRutaControladorVista
ListarGET/productsindex()index.blade.php
CrearGET/products/createcreate()form.blade.php
GuardarPOST/productsstore()-
VerGET/products/{id}show()show.blade.php
EditarGET/products/{id}/editedit()form.blade.php
ActualizarPUT/PATCH/products/{id}update()-
EliminarDELETE/products/{id}destroy()-

Comandos Útiles

bash
# Crear todas las piezas del CRUD
php artisan make:model Product -m
php artisan make:controller ProductController --resource --model=Product
php artisan make:seeder ProductTableSeeder

# Ejecutar migraciones y seeders
php artisan migrate:fresh --seed

# Ejecutar servidor de desarrollo
php artisan serve

# Ver rutas
php artisan route:list

¡Felicidades! Has completado un CRUD completo en Laravel 12. Este es el fundamento de cualquier aplicación web que interactúe con base de datos. ¡Ahora estás listo para construir aplicaciones más complejas

Comentarios

Entradas más populares de este blog

Tutorial CRUD con Laravel 12-Pastel::factory(50)->create();

CRUD en Laravel 12 con Git - Guía para Principiantes