Tutorial CRUD con Laravel 12-Pastel::factory(50)->create();
Introducción
Este tutorial está diseñado para enseñarte a crear un sistema CRUD (Create, Read, Update, Delete) completo en Laravel 12, utilizando las últimas características y mejores prácticas del framework. A diferencia del tutorial original (diseñado para Laravel 5), esta versión incorpora las novedades de Laravel 12 y un enfoque más moderno.
Índice
| Propósito | Comando |
|---|---|
| 1-Configuración inicial | composer create-project laravel/laravel nombre-proyecto |
| 2-Creación de la migración | php artisan make:migration create_pasteles_table |
| 3-Creación del seeder | php artisan make:seeder PastelSeeder |
| 4-Creación del modelo | php artisan make:model Pastel |
| 5-Creación del controlador | php artisan make:controller PastelController |
| 6-Definición de rutas | php artisan route:list |
| 7-Creación de vistas | (Crear manualmente en resources/views/) |
| 8-Mejores prácticas y optimizaciones | php artisan optimize |
1-Configuración inicial
Antes de comenzar, asegúrate de tener instalado:
# Verifica tu versión de PHP (mínimo 8.2 para Laravel 12)
php -v
# Verifica tu versión de Composer
composer -V
# Crea un nuevo proyecto Laravel
composer create-project laravel/laravel pasteles-app
cd pasteles-app
# Configura tu archivo .env con los datos de tu base de datos
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pasteles_db
DB_USERNAME=root
DB_PASSWORD=2-Creación de la migración
Crearemos la migración para la tabla pasteles:
php artisan make:migration create_pasteles_tableActualización para Laravel 12:
Laravel 12 usa
Schemacon nuevas características comojson(),uuid(), y mejoras enenum().Usamos
foreignId()para relaciones futuras.
<?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('pasteles', function (Blueprint $table) { $table->id(); // Equivalente a increments('id') pero más moderno $table->string('nombre', 100); $table->enum('sabor', ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']); $table->decimal('precio', 8, 2)->nullable(); // Nuevo campo añadido $table->boolean('disponible')->default(true); $table->text('descripcion')->nullable(); $table->timestamps(); $table->softDeletes(); // Añadimos soft deletes para eliminación segura }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('pasteles'); } };
Novedades de Laravel 12:
$table->id()es la forma moderna y recomendada.softDeletes()permite recuperar registros eliminados.Uso de tipos de datos mejorados.
Ejecución de las migraciones
Una vez definida la migración, estos son los comandos que usarás en el día a día:
| Comando | ¿Qué hace? |
|---|---|
php artisan migrate | Ejecuta todas las migraciones pendientes (crea la tabla en la BD). |
php artisan migrate:fresh | Elimina todas las tablas y vuelve a ejecutar todas las migraciones desde cero. Útil para reiniciar el proyecto en desarrollo. |
php artisan migrate:refresh | Hace un rollback (reversión) de todas las migraciones y luego ejecuta migrate de nuevo. Es como un "reinicio suave" que conserva los datos de los seeders si los tienes. |
php artisan migrate:rollback | Revierte solo el último lote de migraciones (las que se ejecutaron en la última tanda). |
php artisan migrate:status | Muestra el estado de todas las migraciones (cuáles están ejecutadas y cuáles pendientes). |
⚡ Bonus combinado (muy útil)
Si además quieres poblar la tabla con datos de prueba (seeders) justo después de reiniciar:
php artisan migrate:fresh --seedphp artisan migrate:refresh --seedAmbos hacen lo mismo que sus versiones sin --seed, pero además ejecutan los DatabaseSeeder y tus seeders personalizados para tener datos de prueba inmediatamente.
Si tu servicio PHP se llama app, entonces ejecuta:
docker compose exec app php artisan make:controller PastelControllerEsto hace exactamente lo que querías, pero ejecuta PHP dentro del contenedor.
Si tu contenedor realmente se llama laravel-docker, puedes hacer:
docker exec -it laravel-docker /bin/bashUna vez dentro, verás algo parecido a:
root@abc123:/var/www/html#Entonces ya puedes ejecutar:
php artisan make:controller PastelControllerCon esto ya tienes el flujo completo: crear la migración → ejecutarla → y saber cómo deshacer o reiniciar cuando sea necesario
3-Creación del seeder
php artisan make:seeder PastelSeederActualización para Laravel 12:
Uso de factory en lugar de inserciones manuales.
Mejores prácticas con Faker v3.
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Pastel;
class PastelSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Usamos el factory para crear registros
Pastel::factory(50)->create();
}
}Crear el Factory:
php artisan make:factory PastelFactory<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Pastel>
*/
class PastelFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
return [
'nombre' => 'Pastel ' . $this->faker->unique()->firstName() . ' ' . $this->faker->lastName(),
'sabor' => $this->faker->randomElement($sabores),
'precio' => $this->faker->randomFloat(2, 50, 500),
'disponible' => $this->faker->boolean(80), // 80% de probabilidad
'descripcion' => $this->faker->sentence(10),
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
'updated_at' => $this->faker->dateTimeBetween('-1 month', '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([
PastelSeeder::class,
]);
}
}Ejecutar migraciones y seeders:
php artisan migrate --seed
# O por separado:
php artisan migrate
php artisan db:seed4-Creación del modelo
php artisan make:model PastelModelo básico para principiantes
app/Models/Pastel.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Pastel extends Model
{
use HasFactory, SoftDeletes;
// Especificamos la tabla (opcional si sigue la convención)
protected $table = 'pasteles';
// Campos que pueden ser asignados masivamente
protected $fillable = [
'nombre',
'sabor',
'precio',
'disponible',
'descripcion'
];
}Explicación para principiantes:
| Línea | Explicación |
|---|---|
use HasFactory, SoftDeletes; | Permite usar fábricas para crear datos de prueba y eliminación lógica (no borra el registro, solo lo oculta) |
protected $table = 'pasteles'; | Le dice a Laravel que use la tabla pasteles (si no lo pones, Laravel buscaría una tabla llamada pastels por convención) |
protected $fillable = [...] | Especifica qué campos pueden ser llenados cuando creas o actualizas un pastel. Esto es importante por seguridad (protección contra asignación masiva) |
📌 Guardar commit del modelo
git add app/Models/Pastel.php
git commit -m "feat: crear modelo Pastel con SoftDeletes y fillable"¿Qué necesitas agregar más adelante?
A medida que avances, puedes ir agregando:
| Característica | Cuándo agregarla |
|---|---|
$casts | Cuando necesites que los tipos de datos se conviertan automáticamente (ej: precio a decimal) |
scopes | Cuando necesites consultas reutilizables (ej: Pastel::disponibles()->get()) |
| Accesors / Mutators | Cuando necesites formatear datos al mostrarlos o guardarlos |
Pero por ahora, solo con $fillable es suficiente para que el CRUD funcione.
Actualización para Laravel 12:
Uso de
$fillableo$guardedpara protección masiva.Definición de casts para tipos de datos.
Relaciones y scopes.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Pastel extends Model
{
use HasFactory, SoftDeletes;
// Especificamos la tabla (no necesario si sigue la convención)
protected $table = 'pasteles';
// Campos que pueden ser asignados masivamente
protected $fillable = [
'nombre',
'sabor',
'precio',
'disponible',
'descripcion'
];
// Casts para conversión automática de tipos
protected $casts = [
'precio' => 'decimal:2',
'disponible' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Atributos que siempre deben ser incluidos en arrays/JSON
protected $visible = [
'id',
'nombre',
'sabor',
'precio',
'disponible',
'descripcion',
'created_at',
'updated_at'
];
// Scopes para consultas comunes
public function scopeDisponibles($query)
{
return $query->where('disponible', true);
}
public function scopeBySabor($query, $sabor)
{
return $query->where('sabor', $sabor);
}
// Accesors para formatear datos
public function getNombreCompletoAttribute(): string
{
return $this->nombre . ' (' . $this->sabor . ')';
}
// Mutators para modificar datos antes de guardar
public function setNombreAttribute($value): void
{
$this->attributes['nombre'] = ucwords(strtolower($value));
}
}Novedades de Laravel 12:
SoftDeletespara eliminación lógica.castscon tipos más precisos.Scopes para consultas reutilizables.
Accesors y mutators para manipulación de atributos.
5-1-Crear el controlador
php artisan make:controller PastelController --resource--resource :: Crea todas las funciones crud:ublic function index() { ... } public function create() { ... } public function store() { ... } public function show() { ... } public function edit() { ... } public function update() { ... } public function destroy() { ... }
class PastelController extends Controller { public function index() { return "Hola mundo"; } }
Delo lo contrario el controlador se crea vacío:<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class PastelController extends Controller { // ¡No tiene ningún método! }
si quiero usarlo en las rutas utilizo el namespace:
use App\Http\Controllers\PastelController;
Controlador básico sin FormRequest
app/Http/Controllers/PastelController.php
<?php
namespace App\Http\Controllers;
use App\Models\Pastel;
use Illuminate\Http\Request;
class PastelController extends Controller
{
/**
* Muestra la lista de pasteles.
*Route::get('/pasteles', [PastelController::class, 'index'])->name('pasteles.index');
*/
public function index()
{
$pasteles = Pastel::all(); // Obtiene todos los pasteles
return view('pasteles.index', compact('pasteles'));
}
/**
* Muestra el formulario para crear un nuevo pastel.
*Route::get('/pasteles/create', [PastelController::class, 'create'])->name('pasteles.create');
*
*/
public function create()
{
return view('pasteles.create');
}
/**
* Almacena un nuevo pastel en la base de datos.
*Route::post('/pasteles', [PastelController::class, 'store'])->name('pasteles.store');
*/
public function store(Request $request)
{
// 1. VALIDAR los datos
$validated = $request->validate([
'nombre' => 'required|string|max:100',
'sabor' => 'required|string|max:50',
'precio' => 'required|numeric|min:0',
'disponible' => 'nullable|boolean',
'descripcion' => 'nullable|string|max:1000',
]);
// 2. GUARDAR en la base de datos
Pastel::create($validated);
// 3. REDIRIGIR con mensaje de éxito
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel creado exitosamente!');
}
/**
* Muestra un pastel específico.
*Route::get('/pasteles/{pastel}', [PastelController::class, 'show'])->name('pasteles.show');
*/
public function show(Pastel $pastel)
{
return view('pasteles.show', compact('pastel'));
}
/**
* Muestra el formulario para editar un pastel.
*Route::get('/pasteles/{pastel}/edit', [PastelController::class, 'edit'])->name('pasteles.edit');
*/
public function edit(Pastel $pastel)
{
return view('pasteles.edit', compact('pastel'));
}
/**
* Actualiza un pastel específico.
*Route::put('/pasteles/{pastel}', [PastelController::class, 'update'])->name('pasteles.update');
*/
public function update(Request $request, Pastel $pastel)
{
// 1. VALIDAR los datos
$validated = $request->validate([
'nombre' => 'required|string|max:100',
'sabor' => 'required|string|max:50',
'precio' => 'required|numeric|min:0',
'disponible' => 'nullable|boolean',
'descripcion' => 'nullable|string|max:1000',
]);
// 2. ACTUALIZAR en la base de datos
$pastel->update($validated);
// 3. REDIRIGIR con mensaje de éxito
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel actualizado exitosamente!');
}
/**
* Elimina (eliminación lógica) un pastel.
*Route::delete('/pasteles/{pastel}', [PastelController::class, 'destroy'])->name('pasteles.destroy');
*/
public function destroy(Pastel $pastel)
{
$pastel->delete();
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel eliminado exitosamente!');
}
}📌 Guardar commit del controlador
git add app/Http/Controllers/PastelController.php
git commit -m "feat: crear PastelController con validación básica"Rutas para este controlador
routes/web.php
use App\Http\Controllers\PastelController;
Las rutas completas (sin comentarios)
use App\Http\Controllers\PastelController;
Route::get('/pasteles', [PastelController::class, 'index'])->name('pasteles.index');
Route::get('/pasteles/create', [PastelController::class, 'create'])->name('pasteles.create');
Route::post('/pasteles', [PastelController::class, 'store'])->name('pasteles.store');
Route::get('/pasteles/{pastel}', [PastelController::class, 'show'])->name('pasteles.show');
Route::get('/pasteles/{pastel}/edit', [PastelController::class, 'edit'])->name('pasteles.edit');
Route::put('/pasteles/{pastel}', [PastelController::class, 'update'])->name('pasteles.update');
Route::delete('/pasteles/{pastel}', [PastelController::class, 'destroy'])->name('pasteles.destroy');
use App\Http\Controllers\PastelController; Route::get('/pasteles', [PastelController::class, 'index'])->name('pasteles.index'); Route::get('/pasteles/create', [PastelController::class, 'create'])->name('pasteles.create'); Route::post('/pasteles', [PastelController::class, 'store'])->name('pasteles.store'); Route::get('/pasteles/{pastel}', [PastelController::class, 'show'])->name('pasteles.show'); Route::get('/pasteles/{pastel}/edit', [PastelController::class, 'edit'])->name('pasteles.edit'); Route::put('/pasteles/{pastel}', [PastelController::class, 'update'])->name('pasteles.update'); Route::delete('/pasteles/{pastel}', [PastelController::class, 'destroy'])->name('pasteles.destroy');
🔄 Versión más corta con Route::resource()
use App\Http\Controllers\PastelController;
Route::resource('pasteles', PastelController::class);
¡Hace exactamente lo mismo
use App\Http\Controllers\PastelController; Route::resource('pasteles', PastelController::class);
Comando para ver todas las rutas en la terminal
php artisan route:list
php artisan route:list
5-2-Creación del controlador
php artisan make:controller PastelController --resourceActualización para Laravel 12:
Uso de
FormRequestpara validación.Inyección de dependencias.
Principios SOLID y mejores prácticas.
<?php
namespace App\Http\Controllers;
use App\Models\Pastel;
use App\Http\Requests\StorePastelRequest;
use App\Http\Requests\UpdatePastelRequest;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PastelController extends Controller
{
/**
* Muestra la lista de pasteles.
*/
public function index(): View
{
$pasteles = Pastel::withTrashed()->paginate(10); // Incluye eliminados
return view('pasteles.index', compact('pasteles'));
}
/**
* Muestra el formulario para crear un nuevo pastel.
*/
public function create(): View
{
$sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
return view('pasteles.create', compact('sabores'));
}
/**
* Almacena un nuevo pastel en la base de datos.
*/
public function store(StorePastelRequest $request): RedirectResponse
{
// Los datos ya están validados por StorePastelRequest
Pastel::create($request->validated());
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel creado exitosamente!');
}
/**
* Muestra un pastel específico.
*/
public function show(Pastel $pastel): View
{
return view('pasteles.show', compact('pastel'));
}
/**
* Muestra el formulario para editar un pastel.
*/
public function edit(Pastel $pastel): View
{
$sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
return view('pasteles.edit', compact('pastel', 'sabores'));
}
/**
* Actualiza un pastel específico.
*/
public function update(UpdatePastelRequest $request, Pastel $pastel): RedirectResponse
{
$pastel->update($request->validated());
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel actualizado exitosamente!');
}
/**
* Elimina (o elimina lógicamente) un pastel.
*/
public function destroy(Pastel $pastel): RedirectResponse
{
$pastel->delete();
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel eliminado exitosamente!');
}
}Crear Form Requests para validación:
php artisan make:request StorePastelRequest
php artisan make:request UpdatePastelRequest<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePastelRequest extends FormRequest
{
/**
* Determina si el usuario está autorizado.
*/
public function authorize(): bool
{
return true; // Cambiar según lógica de autenticación
}
/**
* Reglas de validación.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'nombre' => 'required|string|max:100|unique:pasteles,nombre',
'sabor' => 'required|in:chocolate,vainilla,cheesecake,fresa,coco',
'precio' => 'nullable|numeric|min:0|max:9999.99',
'disponible' => 'nullable|boolean',
'descripcion' => 'nullable|string|max:1000',
];
}
/**
* Mensajes de error personalizados.
*/
public function messages(): array
{
return [
'nombre.required' => 'El nombre del pastel es obligatorio.',
'nombre.unique' => 'Ya existe un pastel con ese nombre.',
'sabor.required' => 'Debes seleccionar un sabor.',
'sabor.in' => 'El sabor seleccionado no es válido.',
'precio.numeric' => 'El precio debe ser un número.',
'precio.min' => 'El precio no puede ser negativo.',
'descripcion.max' => 'La descripción no puede exceder los 1000 caracteres.',
];
}
}6.Definición de rutas
Actualización para Laravel 12:
Rutas más explícitas y con nombres.
Agrupación por conveniencia.
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PastelController;
Route::get('/', function () {
return redirect()->route('pasteles.index');
});
// Ruta resource con nombres personalizados
Route::resource('pasteles', PastelController::class)
->names([
'index' => 'pasteles.index',
'create' => 'pasteles.create',
'store' => 'pasteles.store',
'show' => 'pasteles.show',
'edit' => 'pasteles.edit',
'update' => 'pasteles.update',
'destroy' => 'pasteles.destroy',
]);
// Rutas adicionales para funcionalidades extra
Route::prefix('pasteles')->group(function () {
Route::get('/sabor/{sabor}', [PastelController::class, 'porSabor'])
->name('pasteles.sabor');
Route::post('/{pastel}/restore', [PastelController::class, 'restore'])
->name('pasteles.restore');
});
// Verificar rutas
// php artisan route:list7-Creación de vistas
Layout principal (resources/views/layouts/app.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>{{ config('app.name', 'Pastelería') }} - @yield('title', 'Panel de Control')</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
@stack('styles')
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ route('pasteles.index') }}">
<i class="fas fa-cake"></i> Pastelería
</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('pasteles.index') }}">
<i class="fas fa-list"></i> Pasteles
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('pasteles.create') }}">
<i class="fas fa-plus"></i> Nuevo Pastel
</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
<!-- Contenido principal -->
<main class="py-4">
<div class="container">
@yield('content')
</div>
</main>
<!-- 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>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
@stack('scripts')
</body>
</html>Vista Index (resources/views/pasteles/index.blade.php)
@extends('layouts.app')
@section('title', 'Lista de Pasteles')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-cake"></i> Lista de Pasteles</h1>
<a href="{{ route('pasteles.create') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Nuevo Pastel
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="pasteles-table">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Sabor</th>
<th>Precio</th>
<th>Disponible</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
@forelse($pasteles as $pastel)
<tr>
<td>{{ $pastel->id }}</td>
<td>
<a href="{{ route('pasteles.show', $pastel) }}" class="text-decoration-none">
{{ $pastel->nombre }}
</a>
</td>
<td>
<span class="badge bg-info">
{{ ucfirst($pastel->sabor) }}
</span>
</td>
<td>${{ number_format($pastel->precio, 2) }}</td>
<td>
@if($pastel->disponible)
<span class="badge bg-success">
<i class="fas fa-check"></i> Disponible
</span>
@else
<span class="badge bg-danger">
<i class="fas fa-times"></i> No disponible
</span>
@endif
</td>
<td>
@if($pastel->trashed())
<span class="badge bg-secondary">Eliminado</span>
@else
<span class="badge bg-primary">Activo</span>
@endif
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ route('pasteles.show', $pastel) }}"
class="btn btn-sm btn-info" title="Ver">
<i class="fas fa-eye"></i>
</a>
<a href="{{ route('pasteles.edit', $pastel) }}"
class="btn btn-sm btn-warning" title="Editar">
<i class="fas fa-edit"></i>
</a>
@if($pastel->trashed())
<form action="{{ route('pasteles.restore', $pastel) }}"
method="POST" style="display:inline;">
@csrf
<button type="submit" class="btn btn-sm btn-success" title="Restaurar">
<i class="fas fa-undo"></i>
</button>
</form>
@else
<form action="{{ route('pasteles.destroy', $pastel) }}"
method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('¿Eliminar este pastel?')" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-info-circle"></i> No hay pasteles registrados
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Paginación -->
<div class="d-flex justify-content-center">
{{ $pasteles->links() }}
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('#pasteles-table').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/es-ES.json'
},
order: [[0, 'desc']],
pageLength: 10,
lengthMenu: [[5, 10, 25, 50], [5, 10, 25, 50]]
});
});
</script>
@endpushVista Create (resources/views/pasteles/create.blade.php)
@extends('layouts.app')
@section('title', 'Crear Nuevo Pastel')
@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-plus-circle"></i> Crear Nuevo Pastel</h5>
</div>
<div class="card-body">
<form action="{{ route('pasteles.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="nombre" class="form-label">Nombre</label>
<input type="text"
class="form-control @error('nombre') is-invalid @enderror"
id="nombre"
name="nombre"
value="{{ old('nombre') }}"
placeholder="Ej: Pastel de Chocolate"
required>
@error('nombre')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sabor" class="form-label">Sabor</label>
<select class="form-select @error('sabor') is-invalid @enderror"
id="sabor"
name="sabor"
required>
<option value="">Selecciona un sabor...</option>
@foreach($sabores as $sabor)
<option value="{{ $sabor }}" {{ old('sabor') == $sabor ? 'selected' : '' }}>
{{ ucfirst($sabor) }}
</option>
@endforeach
</select>
@error('sabor')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="precio" class="form-label">Precio</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number"
class="form-control @error('precio') is-invalid @enderror"
id="precio"
name="precio"
value="{{ old('precio') }}"
step="0.01"
min="0"
placeholder="0.00">
</div>
@error('precio')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control @error('descripcion') is-invalid @enderror"
id="descripcion"
name="descripcion"
rows="3"
placeholder="Describe tu pastel...">{{ old('descripcion') }}</textarea>
@error('descripcion')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3 form-check">
<input type="checkbox"
class="form-check-input"
id="disponible"
name="disponible"
value="1"
{{ old('disponible') ? 'checked' : '' }}>
<label class="form-check-label" for="disponible">
Disponible para venta
</label>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Guardar Pastel
</button>
<a href="{{ route('pasteles.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsectionVista Edit (resources/views/pasteles/edit.blade.php)
@extends('layouts.app')
@section('title', 'Editar Pastel: ' . $pastel->nombre)
@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="fas fa-edit"></i> Editar Pastel</h5>
</div>
<div class="card-body">
<form action="{{ route('pasteles.update', $pastel) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="nombre" class="form-label">Nombre</label>
<input type="text"
class="form-control @error('nombre') is-invalid @enderror"
id="nombre"
name="nombre"
value="{{ old('nombre', $pastel->nombre) }}"
required>
@error('nombre')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sabor" class="form-label">Sabor</label>
<select class="form-select @error('sabor') is-invalid @enderror"
id="sabor"
name="sabor"
required>
<option value="">Selecciona un sabor...</option>
@foreach($sabores as $sabor)
<option value="{{ $sabor }}"
{{ old('sabor', $pastel->sabor) == $sabor ? 'selected' : '' }}>
{{ ucfirst($sabor) }}
</option>
@endforeach
</select>
@error('sabor')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="precio" class="form-label">Precio</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number"
class="form-control @error('precio') is-invalid @enderror"
id="precio"
name="precio"
value="{{ old('precio', $pastel->precio) }}"
step="0.01"
min="0">
</div>
@error('precio')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control @error('descripcion') is-invalid @enderror"
id="descripcion"
name="descripcion"
rows="3">{{ old('descripcion', $pastel->descripcion) }}</textarea>
@error('descripcion')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3 form-check">
<input type="checkbox"
class="form-check-input"
id="disponible"
name="disponible"
value="1"
{{ old('disponible', $pastel->disponible) ? 'checked' : '' }}>
<label class="form-check-label" for="disponible">
Disponible para venta
</label>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-warning">
<i class="fas fa-save"></i> Actualizar Pastel
</button>
<a href="{{ route('pasteles.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsectionVista Show (resources/views/pasteles/show.blade.php)
@extends('layouts.app')
@section('title', 'Detalles de: ' . $pastel->nombre)
@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Detalles del Pastel</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">ID</dt>
<dd class="col-sm-9">{{ $pastel->id }}</dd>
<dt class="col-sm-3">Nombre</dt>
<dd class="col-sm-9">{{ $pastel->nombre }}</dd>
<dt class="col-sm-3">Sabor</dt>
<dd class="col-sm-9">
<span class="badge bg-info">{{ ucfirst($pastel->sabor) }}</span>
</dd>
<dt class="col-sm-3">Precio</dt>
<dd class="col-sm-9">${{ number_format($pastel->precio, 2) }}</dd>
<dt class="col-sm-3">Disponibilidad</dt>
<dd class="col-sm-9">
@if($pastel->disponible)
<span class="badge bg-success">
<i class="fas fa-check"></i> Disponible
</span>
@else
<span class="badge bg-danger">
<i class="fas fa-times"></i> No disponible
</span>
@endif
</dd>
<dt class="col-sm-3">Descripción</dt>
<dd class="col-sm-9">{{ $pastel->descripcion ?? 'Sin descripción' }}</dd>
<dt class="col-sm-3">Creado</dt>
<dd class="col-sm-9">{{ $pastel->created_at->format('d/m/Y H:i') }}</dd>
<dt class="col-sm-3">Actualizado</dt>
<dd class="col-sm-9">{{ $pastel->updated_at->format('d/m/Y H:i') }}</dd>
@if($pastel->deleted_at)
<dt class="col-sm-3">Eliminado</dt>
<dd class="col-sm-9">{{ $pastel->deleted_at->format('d/m/Y H:i') }}</dd>
@endif
</dl>
<div class="d-grid gap-2">
<a href="{{ route('pasteles.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver a la lista
</a>
<a href="{{ route('pasteles.edit', $pastel) }}" class="btn btn-warning">
<i class="fas fa-edit"></i> Editar Pastel
</a>
</div>
</div>
</div>
</div>
</div>
@endsectionAñadir método restore al controlador
/**
* Restaura un pastel eliminado lógicamente.
*/
public function restore($id): RedirectResponse
{
$pastel = Pastel::withTrashed()->findOrFail($id);
$pastel->restore();
return redirect()
->route('pasteles.index')
->with('success', '¡Pastel restaurado exitosamente!');
}8-Mejores prácticas y optimizaciones
1. Optimización de consultas con Eloquent
// Usar eager loading para relaciones
$pasteles = Pastel::with(['categoria', 'ingredientes'])->paginate(10);
// Usar select para solo campos necesarios
$pasteles = Pastel::select('id', 'nombre', 'sabor', 'precio')->get();
// Usar scopes para consultas complejas
$pasteles = Pastel::disponibles()->bySabor('chocolate')->get();2. Caché para mejorar rendimiento
use Illuminate\Support\Facades\Cache;
public function index()
{
$pasteles = Cache::remember('pasteles_list', 3600, function () {
return Pastel::withTrashed()->paginate(10);
});
return view('pasteles.index', compact('pasteles'));
}3. Eventos y Listeners para acciones
// En modelo Pastel
protected $dispatchesEvents = [
'created' => PastelCreated::class,
'updated' => PastelUpdated::class,
'deleted' => PastelDeleted::class,
];4. Middleware para autorización
php artisan make:middleware CheckAdmin5. Políticas para autorización
php artisan make:policy PastelPolicy6. Testing automatizado
# Crear test
php artisan make:test PastelControllerTest
# Ejecutar tests
php artisan test7. Optimización de assets
# Instalar Laravel Mix (ya incluido en Laravel 12)
npm install
# Compilar assets para producción
npm run productionComandos útiles para desarrollo
# Ejecutar el servidor de desarrollo
php artisan serve
# Limpiar caché
php artisan cache:clear
php artisan config:clear
php artisan view:clear
php artisan route:clear
# Crear una nueva migración
php artisan make:migration add_categoria_id_to_pasteles_table
# Ejecutar migraciones específicas
php artisan migrate --path=database/migrations/2024_01_01_000000_create_pasteles_table.php
# Rollback de migraciones
php artisan migrate:rollback
php artisan migrate:reset
# Ver estado de migraciones
php artisan migrate:status
# Crear factory con relaciones
php artisan make:factory PastelFactory --model=Pastel
# Crear un nuevo middleware
php artisan make:middleware CheckUserRoleSolución de problemas comunes
Error: Class "App\Models\Pastel" not found
Ejecutar:
composer dump-autoload
Error: Unknown column 'deleted_at'
Ejecutar:
php artisan migrate:fresh --seed
Error: SQLSTATE[HY000] [1049] Unknown database
Crear la base de datos manualmente
Error: Call to undefined method App\Models\Pastel::factory()
Asegurar que el modelo use
HasFactory
Conclusión
Este tutorial te ha guiado a través de la creación de un CRUD completo en Laravel 12, incorporando las últimas características del framework:
Migraciones modernas con tipos de datos mejorados
Factories y Seeders para datos de prueba
Modelos con SoftDeletes, Scopes y Casts
Form Requests para validación robusta
Vistas con Bootstrap 5 y DataTables
Rutas RESTful con nomenclatura adecuada
Mensajes flash para feedback al usuario
Optimizaciones para mejor rendimiento
El código resultante es más limpio, mantenible y sigue las mejores prácticas actuales de Laravel. Puedes extenderlo añadiendo relaciones, más campos, autenticación, y funcionalidades adicionales según los requisitos de tu proyecto
Comentarios
Publicar un comentario