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

   <a href="{{ route('pasteles.create') }}" class="btn btn-primary">
        <i class="fas fa-plus"></i> Nuevo Pastel
    </a>

 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ósitoComando
1-Configuración inicialcomposer create-project laravel/laravel nombre-proyecto
2-Creación de la migraciónphp artisan make:migration create_pasteles_table
3-Creación del seederphp artisan make:seeder PastelSeeder
4-Creación del modelophp artisan make:model Pastel
5-Creación del controladorphp artisan make:controller PastelController
6-Definición de rutasphp artisan route:list
7-Creación de vistas(Crear manualmente en resources/views/)
8-Mejores prácticas y optimizacionesphp artisan optimize

1-Configuración inicial

Antes de comenzar, asegúrate de tener instalado:

bash
# 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:

bash
php artisan make:migration create_pasteles_table

Actualización para Laravel 12:

  • Laravel 12 usa Schema con nuevas características como json(), uuid(), y mejoras en enum().

  • Usamos foreignId() para relaciones futuras.

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('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 migrateEjecuta todas las migraciones pendientes (crea la tabla en la BD).
php artisan migrate:freshElimina todas las tablas y vuelve a ejecutar todas las migraciones desde cero. Útil para reiniciar el proyecto en desarrollo.
php artisan migrate:refreshHace 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:rollbackRevierte solo el último lote de migraciones (las que se ejecutaron en la última tanda).
php artisan migrate:statusMuestra 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:

bash
php artisan migrate:fresh --seed
bash
php artisan migrate:refresh --seed

Ambos 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:

text
docker compose exec app php artisan make:controller PastelController

Esto hace exactamente lo que querías, pero ejecuta PHP dentro del contenedor.

Si tu contenedor realmente se llama laravel-docker, puedes hacer:

text
docker exec -it laravel-docker /bin/bash

Una vez dentro, verás algo parecido a:

text
root@abc123:/var/www/html#

Entonces ya puedes ejecutar:

text
php artisan make:controller PastelController

Con 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

bash
php artisan make:seeder PastelSeeder

Actualización para Laravel 12:

  • Uso de factory en lugar de inserciones manuales.

  • Mejores prácticas con Faker v3.

php
<?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:

bash
php artisan make:factory PastelFactory
php
<?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
<?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:

bash
php artisan migrate --seed
# O por separado:
php artisan migrate
php artisan db:seed

4-Creación del modelo

bash
php artisan make:model Pastel

Modelo básico para principiantes

app/Models/Pastel.php

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íneaExplicació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

bash
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ísticaCuándo agregarla
$castsCuando necesites que los tipos de datos se conviertan automáticamente (ej: precio a decimal)
scopesCuando necesites consultas reutilizables (ej: Pastel::disponibles()->get())
Accesors / MutatorsCuando 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 $fillable o $guarded para protección masiva.

  • Definición de casts para tipos de datos.

  • Relaciones y scopes.

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 (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:

  • SoftDeletes para eliminación lógica.

  • casts con tipos más precisos.

  • Scopes para consultas reutilizables.

  • Accesors y mutators para manipulación de atributos.

5-1-Crear el controlador

bash
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
<?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
<?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

bash
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)

php
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()

php
use App\Http\Controllers\PastelController;

Route::resource('pasteles', PastelController::class);

¡Hace exactamente lo mismo


Comando para ver todas las rutas en la terminal

bash
php artisan route:list


5-2-Creación del controlador

bash
php artisan make:controller PastelController --resource

Actualización para Laravel 12:

  • Uso de FormRequest para validación.

  • Inyección de dependencias.

  • Principios SOLID y mejores prácticas.

php
<?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:

bash
php artisan make:request StorePastelRequest
php artisan make:request UpdatePastelRequest
php
<?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
<?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:list

7-Creación de vistas

Layout principal (resources/views/layouts/app.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>{{ 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)

blade
@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>
@endpush

Vista Create (resources/views/pasteles/create.blade.php)

blade
@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>
@endsection

Vista Edit (resources/views/pasteles/edit.blade.php)

blade
@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>
@endsection

Vista Show (resources/views/pasteles/show.blade.php)

blade
@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>
@endsection

Añadir método restore al controlador

php
/**
 * 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

php
// 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

php
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

php
// En modelo Pastel
protected $dispatchesEvents = [
    'created' => PastelCreated::class,
    'updated' => PastelUpdated::class,
    'deleted' => PastelDeleted::class,
];

4. Middleware para autorización

bash
php artisan make:middleware CheckAdmin

5. Políticas para autorización

bash
php artisan make:policy PastelPolicy

6. Testing automatizado

bash
# Crear test
php artisan make:test PastelControllerTest

# Ejecutar tests
php artisan test

7. Optimización de assets

bash
# Instalar Laravel Mix (ya incluido en Laravel 12)
npm install

# Compilar assets para producción
npm run production

Comandos útiles para desarrollo

bash
# 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 CheckUserRole

Solució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:

  1. Migraciones modernas con tipos de datos mejorados

  2. Factories y Seeders para datos de prueba

  3. Modelos con SoftDeletes, Scopes y Casts

  4. Form Requests para validación robusta

  5. Vistas con Bootstrap 5 y DataTables

  6. Rutas RESTful con nomenclatura adecuada

  7. Mensajes flash para feedback al usuario

  8. 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

Entradas más populares de este blog

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

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