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

 

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

Este tutorial está diseñado especialmente para principiantes que quieren aprender a crear un sistema CRUD completo en Laravel 12 mientras aprenden a usar Git correctamente. Cada paso incluye instrucciones claras de Git para que nunca te pierdas.


📋 Tabla de Contenidos

  1. Instalación y Git - Paso a Paso

  2. Crear la Migración - Primer Commit

  3. Crear Factory y Seeder

  4. Crear el Modelo

  5. Crear el Controlador y Form Requests

  6. Definir las Rutas

  7. Crear las Vistas

  8. Flujo de Trabajo Git para Principiantes


🚀 1. Instalación y Git - Paso a Paso

Requisitos Previos

bash
# 1. VERIFICAR INSTALACIONES
# Abre tu terminal y escribe:

# Verificar PHP (debe ser 8.2 o superior)
php -v

# Verificar Composer
composer -V

# Verificar Git (si no está instalado, descárgalo de https://git-scm.com)
git --version

Crear el Proyecto Laravel

bash
# 2. CREAR EL PROYECTO
# Esto crea una carpeta llamada "pasteles-app" con Laravel 12
composer create-project laravel/laravel pasteles-app

# 3. ENTRAR A LA CARPETA
cd pasteles-app

📁 INICIALIZAR GIT (¡IMPORTANTE!)

bash
# 4. INICIALIZAR GIT
# Esto convierte tu carpeta en un repositorio Git
git init

# 5. CREAR EL ARCHIVO .gitignore (para que Git ignore archivos innecesarios)
# Abre tu editor de código y crea un archivo llamado ".gitignore" en la raíz
# Copia y pega TODO el contenido de abajo 👇

📄 Archivo .gitignore COMPLETO para Laravel

gitignore
# ==========================================
# ARCHIVOS .GITIGNORE PARA LARAVEL 12
# ==========================================

# Composer (dependencias de PHP)
/vendor/
composer.phar

# Node.js (dependencias de JavaScript)
/node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock

# Archivos de entorno (¡NUNCA subir .env a Git!)
.env
.env.*
!.env.example

# Archivos de caché de Laravel
/storage/*.key
/storage/framework/cache/data/
/storage/framework/sessions/
/storage/framework/views/
/storage/logs/
bootstrap/cache/*.php
bootstrap/cache/*.json

# Archivos públicos compilados
/public/hot
/public/storage
/public/build/
public/css/
public/js/
public/mix-manifest.json

# Archivos del sistema operativo
.DS_Store
Thumbs.db
desktop.ini

# Editores de código
.idea/
.vscode/
*.sublime-*
*.swp
*.swo

# Tests
/.phpunit.cache
/.phpunit.result.cache
/.phpstorm.meta.php

# Otros
*.log
*.sqlite
*.sqlite-journal
Homestead.json
Homestead.yaml
.env.backup
.php-cs-fixer.cache
.phpunit.result.cache

📝 Explicación del .gitignore para Principiantes

Carpeta/Archivo¿Por qué ignorarlo?
/vendor/Contiene paquetes de PHP que se instalan con Composer, no necesitas subirlos
/node_modules/Paquetes de JavaScript, se instalan con npm
.envContiene tus contraseñas y configuraciones privadas
/storage/Archivos temporales y logs de Laravel
/bootstrap/cache/Archivos en caché que se regeneran automáticamente

Continuar con la Configuración

bash
# 6. AGREGAR TODOS LOS ARCHIVOS A GIT
# Esto prepara todos los archivos para el primer commit
git add .

# 7. VERIFICAR QUÉ ARCHIVOS SE VAN A SUBIR
# (No debería aparecer .env, vendor/ ni node_modules/)
git status

# 8. HACER EL PRIMER COMMIT
# El mensaje debe describir lo que hiciste
git commit -m "feat: iniciar proyecto Laravel 12 con estructura base"

# 9. CONFIGURAR LA RAMA PRINCIPAL
# (Por defecto Git usa 'master', muchos usan 'main' ahora)
git branch -M main

Configurar Base de Datos

bash
# 10. CONFIGURAR BASE DE DATOS
# Abre el archivo .env y edita estas líneas:
# (¡NUNCA subas .env a Git! Ya está en .gitignore)

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pasteles_db
DB_USERNAME=root
DB_PASSWORD=

# 11. CREAR LA BASE DE DATOS EN MYSQL
# Abre MySQL y ejecuta:
# CREATE DATABASE pasteles_db;

# 12. VERIFICAR QUE .ENV NO SE HA AGREGADO A GIT
git status  # .env NO debería aparecer (está en .gitignore)

# 13. CREAR UN COMMIT PARA LA CONFIGURACIÓN
# (Solo subimos .env.example, no .env)
git add .env.example
git commit -m "chore: agregar ejemplo de configuración de entorno"

📊 2. Crear la Migración - Primer Commit

Crear la Migración

bash
# 14. CREAR LA MIGRACIÓN
# Esto crea un archivo en database/migrations/
php artisan make:migration create_pasteles_table

📄 Código de la Migración

php
<?php
// database/migrations/YYYY_MM_DD_HHMMSS_create_pasteles_table.php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     * Esto crea la tabla en la base de datos
     */
    public function up(): void
    {
        Schema::create('pasteles', function (Blueprint $table) {
            $table->id();                              // ID autoincrementable
            $table->string('nombre', 100);            // Nombre del pastel
            $table->enum('sabor', [                    // Sabor (solo estos valores)
                'chocolate', 
                'vainilla', 
                'cheesecake', 
                'fresa', 
                'coco'
            ]);
            $table->decimal('precio', 8, 2)->nullable(); // Precio con 2 decimales
            $table->boolean('disponible')->default(true); // Disponible o no
            $table->text('descripcion')->nullable();    // Descripción
            $table->timestamps();                       // created_at y updated_at
            $table->softDeletes();                      // deleted_at (eliminación lógica)
        });
    }

    /**
     * Reverse the migrations.
     * Esto elimina la tabla si hacemos rollback
     */
    public function down(): void
    {
        Schema::dropIfExists('pasteles');
    }
};

📝 COMMIT de la Migración

bash
# 15. VER ESTADO DE CAMBIOS
git status

# 16. AGREGAR LA MIGRACIÓN
git add database/migrations/

# 17. CREAR COMMIT CON DESCRIPCIÓN
git commit -m "feat: crear migración para tabla pasteles"

# 18. EJECUTAR LA MIGRACIÓN (crea la tabla en la base de datos)
php artisan migrate

# 19. SI TODO FUNCIONA, CREAR UN TAG (marca de hito)
git tag -a v0.1.0 -m "Migración inicial creada y ejecutada"

# 20. VER HISTORIAL DE COMMITS
git log --oneline

📖 Explicación para Principiantes

Comando¿Qué hace?
git add archivoPrepara el archivo para el commit
git commit -m "mensaje"Guarda los cambios con un mensaje
git statusMuestra qué archivos han cambiado
git log --onelineMuestra el historial de commits
git tagCrea una marca importante en tu historial

🍰 3. Crear Factory y Seeder

Crear el Factory

bash
# 21. CREAR FACTORY (genera datos falsos para pruebas)
php artisan make:factory PastelFactory
php
<?php
// database/factories/PastelFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PastelFactory extends Factory
{
    /**
     * Define los datos falsos que se generarán
     */
    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),
            'descripcion' => $this->faker->sentence(10),
            'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
            'updated_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
        ];
    }
}

Crear el Seeder

bash
# 22. CREAR SEEDER (llena la tabla con datos)
php artisan make:seeder PastelSeeder
php
<?php
// database/seeders/PastelSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Pastel;

class PastelSeeder extends Seeder
{
    /**
     * Ejecuta el seeder
     */
    public function run(): void
    {
        // Crea 50 pasteles usando el factory
        Pastel::factory(50)->create();
    }
}

Actualizar DatabaseSeeder

php
<?php
// database/seeders/DatabaseSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Ejecuta todos los seeders
     */
    public function run(): void
    {
        $this->call([
            PastelSeeder::class,  // Llamamos a nuestro seeder
        ]);
    }
}

📝 COMMIT de Factory y Seeder

bash
# 23. AGREGAR FACTORY Y SEEDER
git add database/factories/PastelFactory.php
git add database/seeders/PastelSeeder.php
git add database/seeders/DatabaseSeeder.php

# 24. COMMIT CON DESCRIPCIÓN
git commit -m "feat: crear factory y seeder para datos de prueba"

# 25. EJECUTAR MIGRACIONES Y SEEDERS (llena la base de datos)
php artisan migrate --seed

# 26. VERIFICAR QUE LOS DATOS SE INSERTARON
# Abre MySQL y ejecuta:
# USE pasteles_db;
# SELECT * FROM pasteles;

# 27. TAG DE ESTE HITO
git tag -a v0.2.0 -m "Seeders y factories funcionando con 50 registros"

📖 ¿Qué es un Factory y un Seeder?

ConceptoExplicaciónEjemplo
FactoryGenera datos falsos para pruebasCrea nombres, precios, fechas aleatorias
SeederLlena la base de datos con datosUsa factories para insertar 50 registros
DatabaseSeederControla todos los seedersOrquesta qué seeders ejecutar

📦 4. Crear el Modelo

Crear el Modelo

bash
# 28. CREAR MODELO (representa la tabla en el código)
php artisan make:model Pastel
php
<?php
// app/Models/Pastel.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Pastel extends Model
{
    // Estas "características" le dan funcionalidades extra
    use HasFactory, SoftDeletes;

    // La tabla que usa este modelo (si no sigue la convención)
    protected $table = 'pasteles';

    // Campos que se pueden guardar masivamente
    // (Protección contra asignación masiva)
    protected $fillable = [
        'nombre',
        'sabor',
        'precio',
        'disponible',
        'descripcion'
    ];

    // Conversión automática de tipos de datos
    protected $casts = [
        'precio' => 'decimal:2',
        'disponible' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    // Campos visibles al convertir a JSON/Array
    protected $visible = [
        'id',
        'nombre',
        'sabor',
        'precio',
        'disponible',
        'descripcion',
        'created_at',
        'updated_at'
    ];

    // ==========================================
    // SCOPES (consultas predefinidas)
    // ==========================================

    // Filtra pasteles disponibles
    public function scopeDisponibles($query)
    {
        return $query->where('disponible', true);
    }

    // Filtra por sabor
    public function scopeBySabor($query, $sabor)
    {
        return $query->where('sabor', $sabor);
    }

    // ==========================================
    // ACCESORS (formatean datos al leer)
    // ==========================================

    // Devuelve nombre + sabor entre paréntesis
    public function getNombreCompletoAttribute(): string
    {
        return $this->nombre . ' (' . $this->sabor . ')';
    }

    // ==========================================
    // MUTATORS (modifican datos al guardar)
    // ==========================================

    // Capitaliza la primera letra del nombre
    public function setNombreAttribute($value): void
    {
        $this->attributes['nombre'] = ucwords(strtolower($value));
    }
}

📝 COMMIT del Modelo

bash
# 29. AGREGAR EL MODELO
git add app/Models/Pastel.php

# 30. COMMIT CON DESCRIPCIÓN
git commit -m "feat: crear modelo Pastel con SoftDeletes y scopes"

# 31. VER EL ESTADO DEL PROYECTO
git status

# 32. VER EL HISTORIAL COMPLETO
git log --oneline --graph --all

📖 Explicación del Modelo

Elemento¿Qué hace?Ejemplo de uso
$fillableCampos permitidos para guardar en masaPastel::create($request->all())
$castsConvierte tipos automáticamenteEl precio se convierte a decimal
SoftDeletesPermite eliminar sin borrar de la BD$pastel->delete() guarda en deleted_at
scopeConsultas predefinidasPastel::disponibles()->get()
accessorFormatea datos al leer$pastel->nombre_completo
mutatorModifica datos al guardarSiempre guarda el nombre capitalizado

🎮 5. Crear el Controlador y Form Requests

Crear los Form Requests (Validación)

bash
# 33. CREAR FORM REQUESTS PARA VALIDACIÓN
php artisan make:request StorePastelRequest
php artisan make:request UpdatePastelRequest

📄 StorePastelRequest (validación al crear)

php
<?php
// app/Http/Requests/StorePastelRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePastelRequest extends FormRequest
{
    /**
     * ¿Quién puede usar esta validación?
     * En proyectos básicos, siempre true
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Reglas de validación
     */
    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 (en español)
     */
    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.',
        ];
    }
}

📄 Crear el Controlador

bash
# 34. CREAR CONTROLADOR CON OPERACIONES CRUD
php artisan make:controller PastelController --resource
php
<?php
// app/Http/Controllers/PastelController.php

namespace App\Http\Controllers;

use App\Models\Pastel;
use App\Http\Requests\StorePastelRequest;
use App\Http\Requests\UpdatePastelRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class PastelController extends Controller
{
    /**
     * Mostrar todos los pasteles (READ - Listar)
     */
    public function index(): View
    {
        // Obtener todos los pasteles (incluyendo eliminados)
        $pasteles = Pastel::withTrashed()->paginate(10);
        return view('pasteles.index', compact('pasteles'));
    }

    /**
     * Mostrar formulario para crear (CREATE - Formulario)
     */
    public function create(): View
    {
        $sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
        return view('pasteles.create', compact('sabores'));
    }

    /**
     * Guardar nuevo pastel en la base de datos (CREATE - Guardar)
     */
    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!');
    }

    /**
     * Mostrar un pastel específico (READ - Ver detalles)
     */
    public function show(Pastel $pastel): View
    {
        return view('pasteles.show', compact('pastel'));
    }

    /**
     * Mostrar formulario para editar (UPDATE - Formulario)
     */
    public function edit(Pastel $pastel): View
    {
        $sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
        return view('pasteles.edit', compact('pastel', 'sabores'));
    }

    /**
     * Actualizar pastel en la base de datos (UPDATE - Guardar)
     */
    public function update(UpdatePastelRequest $request, Pastel $pastel): RedirectResponse
    {
        $pastel->update($request->validated());
        
        return redirect()
            ->route('pasteles.index')
            ->with('success', '¡Pastel actualizado exitosamente!');
    }

    /**
     * Eliminar pastel (DELETE - Eliminación lógica)
     */
    public function destroy(Pastel $pastel): RedirectResponse
    {
        $pastel->delete();
        
        return redirect()
            ->route('pasteles.index')
            ->with('success', '¡Pastel eliminado exitosamente!');
    }

    /**
     * Restaurar pastel eliminado
     */
    public function restore($id): RedirectResponse
    {
        $pastel = Pastel::withTrashed()->findOrFail($id);
        $pastel->restore();
        
        return redirect()
            ->route('pasteles.index')
            ->with('success', '¡Pastel restaurado exitosamente!');
    }
}

📝 COMMIT del Controlador y Requests

bash
# 35. AGREGAR CONTROLADOR Y REQUESTS
git add app/Http/Controllers/PastelController.php
git add app/Http/Requests/

# 36. COMMIT
git commit -m "feat: crear PastelController con CRUD completo y Form Requests"

# 37. VERIFICAR QUE NO HAY ERRORES
php artisan route:list

# 38. TAG
git tag -a v0.3.0 -m "Controlador CRUD y validaciones completas"

📖 Explicación del CRUD

AcciónMétodo HTTPRutaMétodo en Controlador
CreateGET/pasteles/createcreate()
Create (guardar)POST/pastelesstore()
Read (listar)GET/pastelesindex()
Read (ver)GET/pasteles/{id}show()
Update (formulario)GET/pasteles/{id}/editedit()
Update (guardar)PUT/PATCH/pasteles/{id}update()
DeleteDELETE/pasteles/{id}destroy()

🛤️ 6. Definir las Rutas

Actualizar routes/web.php

php
<?php
// routes/web.php

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

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
*/

// La raíz redirige al listado de pasteles
Route::get('/', function () {
    return redirect()->route('pasteles.index');
});

// CREACIÓN DEL CRUD COMPLETO CON UNA SOLA LÍNEA
// Esto crea automáticamente todas las rutas del CRUD
Route::resource('pasteles', PastelController::class);

// RUTA ADICIONAL PARA RESTAURAR PASTELES ELIMINADOS
// (No está incluida en resource, la agregamos manualmente)
Route::post('/pasteles/{pastel}/restore', [PastelController::class, 'restore'])
    ->name('pasteles.restore');

📝 COMMIT de Rutas

bash
# 39. AGREGAR RUTAS
git add routes/web.php

# 40. COMMIT
git commit -m "feat: definir rutas RESTful para el CRUD de pasteles"

# 41. VERIFICAR TODAS LAS RUTAS (Muy útil para principiantes)
php artisan route:list

# 42. TAG
git tag -a v0.4.0 -m "Rutas completas configuradas"

📖 Nombres de Rutas Generados

VerboURLNombre de RutaControlador@método
GET/pastelespasteles.indexindex()
GET/pasteles/createpasteles.createcreate()
POST/pastelespasteles.storestore()
GET/pasteles/{pastel}pasteles.showshow()
GET/pasteles/{pastel}/editpasteles.editedit()
PUT/PATCH/pasteles/{pastel}pasteles.updateupdate()
DELETE/pasteles/{pastel}pasteles.destroydestroy()

🎨 7. Crear las Vistas

Estructura de Vistas

bash
# 43. CREAR CARPETAS PARA VISTAS
mkdir -p resources/views/layouts
mkdir -p resources/views/pasteles

# 44. CREAR ARCHIVOS DE VISTA
touch resources/views/layouts/app.blade.php
touch resources/views/pasteles/index.blade.php
touch resources/views/pasteles/create.blade.php
touch resources/views/pasteles/edit.blade.php
touch resources/views/pasteles/show.blade.php

📄 Layout Principal (app.blade.php)

blade
{{-- 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 (iconos) -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    
    <!-- DataTables (tablas interactivas) -->
    <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 (Listado de Pasteles)

blade
{{-- 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">
                                    <!-- Botón Ver -->
                                    <a href="{{ route('pasteles.show', $pastel) }}" 
                                       class="btn btn-sm btn-info" title="Ver">
                                        <i class="fas fa-eye"></i>
                                    </a>
                                    
                                    <!-- Botón Editar -->
                                    <a href="{{ route('pasteles.edit', $pastel) }}" 
                                       class="btn btn-sm btn-warning" title="Editar">
                                        <i class="fas fa-edit"></i>
                                    </a>
                                    
                                    <!-- Botón Restaurar o Eliminar -->
                                    @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 (Crear Pastel)

blade
{{-- 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
                    
                    <!-- Campo: Nombre -->
                    <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>

                    <!-- Campo: Sabor -->
                    <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>

                    <!-- Campo: Precio -->
                    <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>

                    <!-- Campo: Descripción -->
                    <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>

                    <!-- Checkbox: Disponible -->
                    <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>

                    <!-- Botones -->
                    <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 (Editar Pastel)

blade
{{-- 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')
                    
                    <!-- Campo: Nombre -->
                    <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>

                    <!-- Campo: Sabor -->
                    <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>

                    <!-- Campo: Precio -->
                    <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>

                    <!-- Campo: Descripción -->
                    <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>

                    <!-- Checkbox: Disponible -->
                    <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>

                    <!-- Botones -->
                    <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 (Ver Detalles)

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

📝 COMMIT de las Vistas

bash
# 45. AGREGAR TODAS LAS VISTAS
git add resources/views/

# 46. COMMIT
git commit -m "feat: crear todas las vistas con Bootstrap 5 y DataTables"

# 47. VERIFICAR QUE EL PROYECTO FUNCIONE
php artisan serve

# 48. ABRIR EL NAVEGADOR EN http://localhost:8000
# Deberías ver el listado de pasteles

# 49. TAG FINAL
git tag -a v1.0.0 -m "CRUD COMPLETO: Pasteles con Laravel 12"

# 50. SUBIR TODO A GITHUB (si tienes repositorio remoto)
git remote add origin https://github.com/tu-usuario/pasteles-app.git
git push -u origin main --tags

📚 8. Flujo de Trabajo Git para Principiantes

🔄 Ciclo de Trabajo Diario

bash
# ==========================================
# FLUJO DE TRABAJO BÁSICO (DÍA A DÍA)
# ==========================================

# 1. VER EL ESTADO ACTUAL
git status

# 2. HACER CAMBIOS EN EL CÓDIGO
# ... editas archivos ...

# 3. VER QUÉ CAMBIOS HICISTE
git diff

# 4. AGREGAR CAMBIOS PARA EL COMMIT
# Opción A: Agregar un archivo específico
git add app/Models/Pastel.php

# Opción B: Agregar todos los archivos modificados
git add .

# 5. CREAR EL COMMIT CON MENSAJE
git commit -m "feat: agregar filtro de búsqueda por nombre"

# 6. SUBIR CAMBIOS (si tienes repositorio remoto)
git push origin main

📝 Guía de Mensajes de Commit

bash
# ==========================================
# TIPOS DE MENSAJES DE COMMIT (Para Principiantes)
# ==========================================

# Nueva funcionalidad
git commit -m "feat: agregar filtro por sabor de pastel"

# Corrección de errores
git commit -m "fix: corregir validación del precio negativo"

# Cambios en documentación
git commit -m "docs: actualizar README con instrucciones de instalación"

# Cambios que no afectan la funcionalidad (formato, estilo)
git commit -m "style: formatear código con PSR-12"

# Refactorización (mejorar código sin cambiar funcionalidad)
git commit -m "refactor: optimizar consultas en PastelController"

# Pruebas
git commit -m "test: agregar pruebas para PastelFactory"

# Tareas de mantenimiento
git commit -m "chore: actualizar dependencias de composer"

🆘 Comandos Git Esenciales para Principiantes

bash
# ==========================================
# COMANDOS BÁSICOS QUE SIEMPRE USARÁS
# ==========================================

# Ver el estado de tus archivos
git status

# Ver qué cambios tienes sin agregar
git diff

# Ver historial de commits
git log --oneline

# Agregar archivo al staging (preparar para commit)
git add archivo.php

# Agregar todos los archivos modificados
git add .

# Crear un commit
git commit -m "mensaje descriptivo"

# Deshacer cambios en un archivo (¡cuidado!)
git checkout -- archivo.php

# Quitar archivo del staging (antes de commit)
git reset HEAD archivo.php

# Ver todas las ramas
git branch

# Crear nueva rama
git checkout -b nombre-rama

# Cambiar de rama
git checkout nombre-rama

📋 Checklist de COMMITS por Etapa

bash
# ==========================================
# COMMITS COMPLETOS DEL TUTORIAL
# ==========================================

# 1. Inicialización del proyecto
git init
git add .
git commit -m "feat: inicializar proyecto Laravel 12"

# 2. Configuración de Git
git add .gitignore
git commit -m "chore: agregar .gitignore para Laravel"

# 3. Configuración de entorno
git add .env.example
git commit -m "chore: agregar ejemplo de configuración de entorno"

# 4. Migración
git add database/migrations/
git commit -m "feat: crear migración para tabla pasteles con softDeletes"

# 5. Factory y Seeder
git add database/factories/
git add database/seeders/
git commit -m "feat: crear factory y seeder para pasteles"

# 6. Modelo
git add app/Models/Pastel.php
git commit -m "feat: crear modelo Pastel con SoftDeletes y scopes"

# 7. Form Requests
git add app/Http/Requests/
git commit -m "feat: crear Form Requests para validación"

# 8. Controlador
git add app/Http/Controllers/PastelController.php
git commit -m "feat: crear PastelController con operaciones CRUD"

# 9. Rutas
git add routes/web.php
git commit -m "feat: definir rutas RESTful para el CRUD"

# 10. Vistas
git add resources/views/
git commit -m "feat: crear vistas completas del CRUD"

🏷️ Tags de Versión

bash
# ==========================================
# CREAR TAGS PARA HITOS IMPORTANTES
# ==========================================

# Tag para versión inicial
git tag -a v0.1.0 -m "Configuración inicial y migraciones"

# Tag para modelo y controlador
git tag -a v0.2.0 -m "Modelo, controlador y rutas"

# Tag para vistas
git tag -a v0.3.0 -m "Vistas completas del CRUD"

# Tag para versión final
git tag -a v1.0.0 -m "CRUD completo con Laravel 12"

# Ver todos los tags
git tag

# Subir tags al repositorio remoto
git push --tags origin main

✅ Resumen Final

📦 Estructura del Proyecto

text
pasteles-app/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── PastelController.php
│   │   └── Requests/
│   │       ├── StorePastelRequest.php
│   │       └── UpdatePastelRequest.php
│   └── Models/
│       └── Pastel.php
├── database/
│   ├── factories/
│   │   └── PastelFactory.php
│   ├── migrations/
│   │   └── YYYY_MM_DD_HHMMSS_create_pasteles_table.php
│   └── seeders/
│       ├── DatabaseSeeder.php
│       └── PastelSeeder.php
├── resources/
│   └── views/
│       ├── layouts/
│       │   └── app.blade.php
│       └── pasteles/
│           ├── index.blade.php
│           ├── create.blade.php
│           ├── edit.blade.php
│           └── show.blade.php
├── routes/
│   └── web.php
├── .env
├── .gitignore
└── README.md (opcional)

🚀 Comandos para Ejecutar el Proyecto

bash
# Iniciar el servidor
php artisan serve

# Ejecutar migraciones
php artisan migrate

# Ejecutar seeders
php artisan db:seed

# Ejecutar migraciones y seeders juntos
php artisan migrate --seed

# Ver todas las rutas
php artisan route:list

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

🎯 ¡Ya tienes tu CRUD completo!

  1. Listar pasteles (index)

  2. Crear pasteles (create/store)

  3. Ver detalles (show)

  4. Editar pasteles (edit/update)

  5. Eliminar pasteles (destroy)

  6. Restaurar pasteles eliminados (restore)

  7. Git control de versiones en cada paso

  8. Buenas prácticas desde el inicio


📚 Recursos Adicionales


¡Felicitaciones! Has completado un CRUD completo en Laravel 12 con Git. 🎉🍰

Ahora puedes extender este proyecto agregando:

  • Autenticación de usuarios

  • Relaciones entre tablas

  • API REST

  • Panel de administración

  • Subida de imágenes

  • Y mucho más...

Comentarios

Entradas más populares de este blog

02 -Rutas en Laravel

01-04-convencion nombres

3-Rutas