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 --version
Crear 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 main
Configurar 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
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📄 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