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
Instalación y Git - Paso a Paso
Crear la Migración - Primer Commit
Crear Factory y Seeder
Crear el Modelo
Crear el Controlador y Form Requests
Definir las Rutas
Crear las Vistas
Flujo de Trabajo Git para Principiantes
🚀 1. Instalación y Git - Paso a Paso
Requisitos Previos
# 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 --versionCrear el Proyecto Laravel
# 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!)
# 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
# ==========================================
# 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 |
.env | Contiene 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
# 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 mainConfigurar Base de Datos
# 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
# 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
// 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
# 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 archivo | Prepara el archivo para el commit |
git commit -m "mensaje" | Guarda los cambios con un mensaje |
git status | Muestra qué archivos han cambiado |
git log --oneline | Muestra el historial de commits |
git tag | Crea una marca importante en tu historial |
🍰 3. Crear Factory y Seeder
Crear el Factory
# 21. CREAR FACTORY (genera datos falsos para pruebas)
php artisan make:factory PastelFactory<?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
# 22. CREAR SEEDER (llena la tabla con datos)
php artisan make:seeder PastelSeeder<?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
// 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
# 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?
| Concepto | Explicación | Ejemplo |
|---|---|---|
| Factory | Genera datos falsos para pruebas | Crea nombres, precios, fechas aleatorias |
| Seeder | Llena la base de datos con datos | Usa factories para insertar 50 registros |
| DatabaseSeeder | Controla todos los seeders | Orquesta qué seeders ejecutar |
📦 4. Crear el Modelo
Creación del modelo
php artisan make:model Pastel
Modelo básico para principiantes
app/Models/Pastel.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Pastel extends Model { use HasFactory, SoftDeletes; // Especificamos la tabla (opcional si sigue la convención) protected $table = 'pasteles'; // Campos que pueden ser asignados masivamente protected $fillable = [ 'nombre', 'sabor', 'precio', 'disponible', 'descripcion' ]; }
Explicación para principiantes:
| Línea | Explicación |
|---|---|
use HasFactory, SoftDeletes; | Permite usar fábricas para crear datos de prueba y eliminación lógica (no borra el registro, solo lo oculta) |
protected $table = 'pasteles'; | Le dice a Laravel que use la tabla pasteles (si no lo pones, Laravel buscaría una tabla llamada pastels por convención) |
protected $fillable = [...] | Especifica qué campos pueden ser llenados cuando creas o actualizas un pastel. Esto es importante por seguridad (protección contra asignación masiva) |
📌 Guardar commit del modelo
git add app/Models/Pastel.php git commit -m "feat: crear modelo Pastel con SoftDeletes y fillable"
¿Qué necesitas agregar más adelante?
A medida que avances, puedes ir agregando:
| Característica | Cuándo agregarla |
|---|---|
$casts | Cuando necesites que los tipos de datos se conviertan automáticamente (ej: precio a decimal) |
scopes | Cuando necesites consultas reutilizables (ej: Pastel::disponibles()->get()) |
| Accesors / Mutators | Cuando necesites formatear datos al mostrarlos o guardarlos |
Pero por ahora, solo con $fillable es suficiente para que el CRUD funcione.
Crear el Modelo
# 28. CREAR MODELO (representa la tabla en el código)
php artisan make:model Pastel<?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
# 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 |
|---|---|---|
$fillable | Campos permitidos para guardar en masa | Pastel::create($request->all()) |
$casts | Convierte tipos automáticamente | El precio se convierte a decimal |
SoftDeletes | Permite eliminar sin borrar de la BD | $pastel->delete() guarda en deleted_at |
scope | Consultas predefinidas | Pastel::disponibles()->get() |
accessor | Formatea datos al leer | $pastel->nombre_completo |
mutator | Modifica datos al guardar | Siempre guarda el nombre capitalizado |
🎮 5. Crear el Controlador y Form Requests
Crear los Form Requests (Validación)
# 33. CREAR FORM REQUESTS PARA VALIDACIÓN
php artisan make:request StorePastelRequest
php artisan make:request UpdatePastelRequest¿Qué es un FormRequest?
Un FormRequest es una clase especial en Laravel que se encarga de dos cosas importantes:
Validar los datos que vienen del formulario (antes de que lleguen al controlador).
Autorizar si el usuario puede o no realizar esa acción.
Es como un "filtro de seguridad" que revisa los datos antes de que entren a tu sistema.
Validar los datos que vienen del formulario (antes de que lleguen al controlador).
Autorizar si el usuario puede o no realizar esa acción.
¿Cómo se usa en el controlador?
Antes (sin FormRequest):
public function store(Request $request)
{
// Validación manual
$validated = $request->validate([
'nombre' => 'required|string|max:100',
'precio' => 'required|numeric|min:0',
]);
Pastel::create($validated);
}
Después (con FormRequest):
public function store(StorePastelRequest $request)
{
// Los datos YA ESTÁN VALIDADOS automáticamente
Pastel::create($request->all());
// o
Pastel::create($request->validated());
}
Diferencia clave: El controlador se vuelve mucho más limpio porque la validación está en otro lado.
public function store(Request $request) { // Validación manual $validated = $request->validate([ 'nombre' => 'required|string|max:100', 'precio' => 'required|numeric|min:0', ]); Pastel::create($validated); }
public function store(StorePastelRequest $request) { // Los datos YA ESTÁN VALIDADOS automáticamente Pastel::create($request->all()); // o Pastel::create($request->validated()); }
📄 StorePastelRequest (validación al crear)
<?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
# 34. CREAR CONTROLADOR CON OPERACIONES CRUD
php artisan make:controller PastelController --resource<?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
# 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ón | Método HTTP | Ruta | Método en Controlador |
|---|---|---|---|
| Create | GET | /pasteles/create | create() |
| Create (guardar) | POST | /pasteles | store() |
| Read (listar) | GET | /pasteles | index() |
| Read (ver) | GET | /pasteles/{id} | show() |
| Update (formulario) | GET | /pasteles/{id}/edit | edit() |
| Update (guardar) | PUT/PATCH | /pasteles/{id} | update() |
| Delete | DELETE | /pasteles/{id} | destroy() |
🛤️ 6. Definir las Rutas
Actualizar routes/web.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
# 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
| Verbo | URL | Nombre de Ruta | Controlador@método |
|---|---|---|---|
| GET | /pasteles | pasteles.index | index() |
| GET | /pasteles/create | pasteles.create | create() |
| POST | /pasteles | pasteles.store | store() |
| GET | /pasteles/{pastel} | pasteles.show | show() |
| GET | /pasteles/{pastel}/edit | pasteles.edit | edit() |
| PUT/PATCH | /pasteles/{pastel} | pasteles.update | update() |
| DELETE | /pasteles/{pastel} | pasteles.destroy | destroy() |
🎨 7. Crear las Vistas
Estructura de Vistas
# 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)
{{-- 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)
{{-- 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)
{{-- 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)
{{-- 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)
{{-- 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
# 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
# ==========================================
# 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
# ==========================================
# 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
# ==========================================
# 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
# ==========================================
# 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
# ==========================================
# 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
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
# 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!
✅ Listar pasteles (index)
✅ Crear pasteles (create/store)
✅ Ver detalles (show)
✅ Editar pasteles (edit/update)
✅ Eliminar pasteles (destroy)
✅ Restaurar pasteles eliminados (restore)
✅ Git control de versiones en cada paso
✅ 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
Publicar un comentario