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
Configuración del Proyecto
Creación de la Migración
Creación del Seeder
Creación del Modelo
Configuración del Enrutador
Creación del Controlador
Creación de las Vistas
Ejecución y Pruebas
Mejoras y Optimizaciones
Ejercicio de Consolidación
1. Configuración del Proyecto
Crear nuevo proyecto Laravel
# 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 datosConfiguración de la base de datos
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
-- 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
php artisan make:migration create_products_tableCódigo de la migración
<?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
php artisan migrate3. Creación del Seeder
Generar el seeder
php artisan make:seeder ProductTableSeederCódigo del seeder
<?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
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
# Ejecutar migración y seeders juntos
php artisan migrate:fresh --seed
# O ejecutar solo el seeder
php artisan db:seed --class=ProductTableSeeder4. Creación del Modelo
Generar el modelo
php artisan make:model ProductCódigo del modelo
<?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)
// 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
php artisan route:list| Método | URI | Nombre | Acción |
|---|---|---|---|
| GET | / | Closure | |
| GET | products | products.index | index |
| GET | products/create | products.create | create |
| POST | products | products.store | store |
| GET | products/{product} | products.show | show |
| GET | products/{product}/edit | products.edit | edit |
| PUT/PATCH | products/{product} | products.update | update |
| DELETE | products/{product} | products.destroy | destroy |
6. Creación del Controlador
Generar el controlador con resource
php artisan make:controller ProductController --resource --model=ProductCódigo del controlador
<?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
resources/views/
├── layouts/
│ └── master.blade.php
└── products/
├── index.blade.php
├── show.blade.php
└── form.blade.phpLayout principal (layouts/master.blade.php)
<!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>© {{ 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)
@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
@endsectionVista: Detalle de producto (products/show.blade.php)
@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>
@endsectionVista: Formulario Crear/Editar (products/form.blade.php)
@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>
@endsection8. Ejecución y Pruebas
Levantar el servidor de desarrollo
php artisan serveProbar la aplicación
Accede a las siguientes URLs para probar el CRUD:
# 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/editComandos de prueba
# 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:clear9. Mejoras y Optimizaciones
1. Agregar validación personalizada
// 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
// 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
// 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
// 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
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest<?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
Crear migración para tabla
categoriesAgregar relación en el modelo Product
Modificar el formulario para incluir categorías
Mostrar categoría en la lista
php artisan make:migration create_categories_table
php artisan make:model Category -m
php artisan make:seeder CategoryTableSeederEjercicio 2: Agregar Imágenes
Agregar campo
image_patha la migración de productsAgregar campo en el formulario (file input)
Implementar subida de archivos
Mostrar imágenes en la vista
Ejercicio 3: Exportar a CSV
Crear método en el controlador para exportar
Generar archivo CSV con datos
Agregar botón de exportación
Ejercicio 4: Dashboard de Estadísticas
Crear vista dashboard
Mostrar estadísticas básicas (total productos, promedio precios, etc.)
Crear gráficos simples con Chart.js
Resumen del CRUD
| Operación | Método HTTP | Ruta | Controlador | Vista |
|---|---|---|---|---|
| Listar | GET | /products | index() | index.blade.php |
| Crear | GET | /products/create | create() | form.blade.php |
| Guardar | POST | /products | store() | - |
| Ver | GET | /products/{id} | show() | show.blade.php |
| Editar | GET | /products/{id}/edit | edit() | form.blade.php |
| Actualizar | PUT/PATCH | /products/{id} | update() | - |
| Eliminar | DELETE | /products/{id} | destroy() | - |
Comandos Útiles
# 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
Publicar un comentario