1

 return redirect()

        ->route('pasteles.index')

        ->with('success', '¡Pastel restaurado exitosamente!');

 <a href="{{ route('pasteles.create') }}" class="btn btn-primary">

        <i class="fas fa-plus"></i> Nuevo Pastel

    </a>


Introducción

Este tutorial está diseñado para enseñarte a crear un sistema CRUD (Create, Read, Update, Delete) completo en Laravel 12, utilizando las últimas características y mejores prácticas del framework. A diferencia del tutorial original (diseñado para Laravel 5), esta versión incorpora las novedades de Laravel 12 y un enfoque más moderno.

Índice

Propósito

Comando

1-Configuración inicial

composer create-project laravel/laravel nombre-proyecto

2-Creación de la migración

php artisan make:migration create_pasteles_table

3-Creación del seeder

php artisan make:seeder PastelSeeder

4-Creación del modelo

php artisan make:model Pastel

5-Creación del controlador

php artisan make:controller PastelController

6-Definición de rutas

php artisan route:list

7-Creación de vistas

(Crear manualmente en resources/views/)

8-Mejores prácticas y optimizaciones

php artisan optimize


1-Configuración inicial

Antes de comenzar, asegúrate de tener instalado:

bash

# Verifica tu versión de PHP (mínimo 8.2 para Laravel 12)

php -v


# Verifica tu versión de Composer

composer -V


# Crea un nuevo proyecto Laravel

composer create-project laravel/laravel pasteles-app

cd pasteles-app


# Configura tu archivo .env con los datos de tu base de datos

DB_CONNECTION=mysql

DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=pasteles_db

DB_USERNAME=root

DB_PASSWORD=

2-Creación de la migración

Crearemos la migración para la tabla pasteles:

bash

php artisan make:migration create_pasteles_table

Actualización para Laravel 12:

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

  • Usamos foreignId() para relaciones futuras.

php

<?php


use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;


return new class extends Migration

{

    /**

     * Run the migrations.

     */

    public function up(): void

    {

        Schema::create('pasteles', function (Blueprint $table) {

            $table->id(); // Equivalente a increments('id') pero más moderno

            $table->string('nombre', 100);

    $table->enum('sabor', ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']);

            $table->decimal('precio', 8, 2)->nullable(); // Nuevo campo añadido

            $table->boolean('disponible')->default(true);

            $table->text('descripcion')->nullable();

            $table->timestamps();

            $table->softDeletes(); // Añadimos soft deletes para eliminación segura

        });

    }


    /**

     * Reverse the migrations.

     */

    public function down(): void

    {

        Schema::dropIfExists('pasteles');

    }

};

Novedades de Laravel 12:

  • $table->id() es la forma moderna y recomendada.

  • softDeletes() permite recuperar registros eliminados.

  • Uso de tipos de datos mejorados.

Ejecución de las migraciones

Una vez definida la migración, estos son los comandos que usarás en el día a día:

Comando

¿Qué hace?

php artisan migrate

Ejecuta todas las migraciones pendientes (crea la tabla en la BD).

php artisan migrate:fresh

Elimina todas las tablas y vuelve a ejecutar todas las migraciones desde cero. Útil para reiniciar el proyecto en desarrollo.

php artisan migrate:refresh

Hace un rollback (reversión) de todas las migraciones y luego ejecuta migrate de nuevo. Es como un "reinicio suave" que conserva los datos de los seeders si los tienes.

php artisan migrate:rollback

Revierte solo el último lote de migraciones (las que se ejecutaron en la última tanda).

php artisan migrate:status

Muestra el estado de todas las migraciones (cuáles están ejecutadas y cuáles pendientes).


⚡ Bonus combinado (muy útil)

Si además quieres poblar la tabla con datos de prueba (seeders) justo después de reiniciar:

bash

php artisan migrate:fresh --seed

bash

php artisan migrate:refresh --seed

Ambos hacen lo mismo que sus versiones sin --seed, pero además ejecutan los DatabaseSeeder y tus seeders personalizados para tener datos de prueba inmediatamente.


Si tu servicio PHP se llama app, entonces ejecuta:

text

docker compose exec app php artisan make:controller PastelController

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

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

text

docker exec -it laravel-docker /bin/bash

Una vez dentro, verás algo parecido a:

text

root@abc123:/var/www/html#

Entonces ya puedes ejecutar:

text

php artisan make:controller PastelController

Con esto ya tienes el flujo completo: crear la migración → ejecutarla → y saber cómo deshacer o reiniciar cuando sea necesario

3-Creación del seeder

bash

php artisan make:seeder PastelSeeder

Actualización para Laravel 12:

  • Uso de factory en lugar de inserciones manuales.

  • Mejores prácticas con Faker v3.

php

<?php


namespace Database\Seeders;


use Illuminate\Database\Seeder;

use App\Models\Pastel;


class PastelSeeder extends Seeder

{

    /**

     * Run the database seeds.

     */

    public function run(): void

    {

        // Usamos el factory para crear registros

        Pastel::factory(50)->create();

    }

}

Crear el Factory:

bash

php artisan make:factory PastelFactory

php

<?php


namespace Database\Factories;


use Illuminate\Database\Eloquent\Factories\Factory;


/**

 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Pastel>

 */

class PastelFactory extends Factory

{

    /**

     * Define the model's default state.

     *

     * @return array<string, mixed>

     */

    public function definition(): array

    {

        $sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];

        

        return [

            'nombre' => 'Pastel ' . $this->faker->unique()->firstName() . ' ' . $this->faker->lastName(),

            'sabor' => $this->faker->randomElement($sabores),

            'precio' => $this->faker->randomFloat(2, 50, 500),

            'disponible' => $this->faker->boolean(80), // 80% de probabilidad

            'descripcion' => $this->faker->sentence(10),

            'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),

            'updated_at' => $this->faker->dateTimeBetween('-1 month', 'now'),

        ];

    }

}

Registrar el seeder en DatabaseSeeder:

php

<?php


namespace Database\Seeders;


use Illuminate\Database\Seeder;


class DatabaseSeeder extends Seeder

{

    /**

     * Seed the application's database.

     */

    public function run(): void

    {

        $this->call([

            PastelSeeder::class,

        ]);

    }

}

Ejecutar migraciones y seeders:

bash

php artisan migrate --seed

# O por separado:

php artisan migrate

php artisan db:seed

4-Creación del modelo

bash

php artisan make:model Pastel

Modelo básico para principiantes

app/Models/Pastel.php

php

<?php


namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;

use Illuminate\Database\Eloquent\SoftDeletes;


class Pastel extends Model

{

    use HasFactory, SoftDeletes;


    // Especificamos la tabla (opcional si sigue la convención)

    protected $table = 'pasteles';


    // Campos que pueden ser asignados masivamente

    protected $fillable = [

        'nombre',

        'sabor',

        'precio',

        'disponible',

        'descripcion'

    ];

}

Explicación para principiantes:

Lí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

bash

git add app/Models/Pastel.php

git commit -m "feat: crear modelo Pastel con SoftDeletes y fillable"


¿Qué necesitas agregar más adelante?

A medida que avances, puedes ir agregando:

Característica

Cuándo agregarla

$casts

Cuando necesites que los tipos de datos se conviertan automáticamente (ej: precio a decimal)

scopes

Cuando necesites consultas reutilizables (ej: Pastel::disponibles()->get())

Accesors / Mutators

Cuando necesites formatear datos al mostrarlos o guardarlos

Pero por ahora, solo con $fillable es suficiente para que el CRUD funcione.

Actualización para Laravel 12:

  • Uso de $fillable o $guarded para protección masiva.

  • Definición de casts para tipos de datos.

  • Relaciones y scopes.

php

<?php


namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;

use Illuminate\Database\Eloquent\SoftDeletes;


class Pastel extends Model

{

    use HasFactory, SoftDeletes;


    // Especificamos la tabla (no necesario si sigue la convención)

    protected $table = 'pasteles';


    // Campos que pueden ser asignados masivamente

    protected $fillable = [

        'nombre',

        'sabor',

        'precio',

        'disponible',

        'descripcion'

    ];


    // Casts para conversión automática de tipos

    protected $casts = [

        'precio' => 'decimal:2',

        'disponible' => 'boolean',

        'created_at' => 'datetime',

        'updated_at' => 'datetime',

    ];


    // Atributos que siempre deben ser incluidos en arrays/JSON

    protected $visible = [

        'id',

        'nombre',

        'sabor',

        'precio',

        'disponible',

        'descripcion',

        'created_at',

        'updated_at'

    ];


    // Scopes para consultas comunes

    public function scopeDisponibles($query)

    {

        return $query->where('disponible', true);

    }


    public function scopeBySabor($query, $sabor)

    {

        return $query->where('sabor', $sabor);

    }


    // Accesors para formatear datos

    public function getNombreCompletoAttribute(): string

    {

        return $this->nombre . ' (' . $this->sabor . ')';

    }


    // Mutators para modificar datos antes de guardar

    public function setNombreAttribute($value): void

    {

        $this->attributes['nombre'] = ucwords(strtolower($value));

    }

}

Novedades de Laravel 12:

  • SoftDeletes para eliminación lógica.

  • casts con tipos más precisos.

  • Scopes para consultas reutilizables.

  • Accesors y mutators para manipulación de atributos.

5-1-Crear el controlador

bash

php artisan make:controller PastelController --resource

--resource :: Crea todas las funciones crud:

ublic function index()    { ... }

public function create()   { ... }

public function store()    { ... }

public function show()     { ... }

public function edit()     { ... }

public function update()   { ... }

public function destroy()  { ... }


class PastelController extends Controller

{

    public function index()

    {

        return "Hola mundo";

    }

}


Delo lo contrario el controlador se crea vacío:

php

<?php


namespace App\Http\Controllers;


use Illuminate\Http\Request;


class PastelController extends Controller

{

    // ¡No tiene ningún método!

}


si quiero usarlo en las rutas utilizo el namespace:

use App\Http\Controllers\PastelController;

Controlador básico sin FormRequest

app/Http/Controllers/PastelController.php

php

<?php


namespace App\Http\Controllers;


use App\Models\Pastel;

use Illuminate\Http\Request;


class PastelController extends Controller

{

    /**

     * Muestra la lista de pasteles.

*Route::get('/pasteles', [PastelController::class, 'index'])->name('pasteles.index');


     */

    public function index()

    {

        $pasteles = Pastel::all(); // Obtiene todos los pasteles

        return view('pasteles.index', compact('pasteles'));

    }


    /**

     * Muestra el formulario para crear un nuevo pastel.

*Route::get('/pasteles/create', [PastelController::class, 'create'])->name('pasteles.create');

*

     */

    public function create()

    {

        return view('pasteles.create');

    }


    /**

     * Almacena un nuevo pastel en la base de datos.

*Route::post('/pasteles', [PastelController::class, 'store'])->name('pasteles.store');


     */

    public function store(Request $request)

    {

        // 1. VALIDAR los datos

        $validated = $request->validate([

            'nombre' => 'required|string|max:100',

            'sabor' => 'required|string|max:50',

            'precio' => 'required|numeric|min:0',

            'disponible' => 'nullable|boolean',

            'descripcion' => 'nullable|string|max:1000',

        ]);


        // 2. GUARDAR en la base de datos

        Pastel::create($validated);


        // 3. REDIRIGIR con mensaje de éxito

        return redirect()

            ->route('pasteles.index')

            ->with('success', '¡Pastel creado exitosamente!');

    }


    /**

     * Muestra un pastel específico.

*Route::get('/pasteles/{pastel}', [PastelController::class, 'show'])->name('pasteles.show');


     */

    public function show(Pastel $pastel)

    {

        return view('pasteles.show', compact('pastel'));

    }


    /**

     * Muestra el formulario para editar un pastel.

*Route::get('/pasteles/{pastel}/edit', [PastelController::class, 'edit'])->name('pasteles.edit');


     */

    public function edit(Pastel $pastel)

    {

        return view('pasteles.edit', compact('pastel'));

    }


    /**

     * Actualiza un pastel específico.

*Route::put('/pasteles/{pastel}', [PastelController::class, 'update'])->name('pasteles.update');


     */

    public function update(Request $request, Pastel $pastel)

    {

        // 1. VALIDAR los datos

        $validated = $request->validate([

            'nombre' => 'required|string|max:100',

            'sabor' => 'required|string|max:50',

            'precio' => 'required|numeric|min:0',

            'disponible' => 'nullable|boolean',

            'descripcion' => 'nullable|string|max:1000',

        ]);


        // 2. ACTUALIZAR en la base de datos

        $pastel->update($validated);


        // 3. REDIRIGIR con mensaje de éxito

        return redirect()

            ->route('pasteles.index')

            ->with('success', '¡Pastel actualizado exitosamente!');

    }


    /**

     * Elimina (eliminación lógica) un pastel.

*Route::delete('/pasteles/{pastel}', [PastelController::class, 'destroy'])->name('pasteles.destroy');


     */

    public function destroy(Pastel $pastel)

    {

        $pastel->delete();


        return redirect()

            ->route('pasteles.index')

            ->with('success', '¡Pastel eliminado exitosamente!');

    }

}


📌 Guardar commit del controlador

bash

git add app/Http/Controllers/PastelController.php

git commit -m "feat: crear PastelController con validación básica"


Rutas para este controlador

routes/web.php

 use App\Http\Controllers\PastelController;

Las rutas completas (sin comentarios)

php

use App\Http\Controllers\PastelController;


Route::get('/pasteles', [PastelController::class, 'index'])->name('pasteles.index');

Route::get('/pasteles/create', [PastelController::class, 'create'])->name('pasteles.create');

Route::post('/pasteles', [PastelController::class, 'store'])->name('pasteles.store');

Route::get('/pasteles/{pastel}', [PastelController::class, 'show'])->name('pasteles.show');

Route::get('/pasteles/{pastel}/edit', [PastelController::class, 'edit'])->name('pasteles.edit');

Route::put('/pasteles/{pastel}', [PastelController::class, 'update'])->name('pasteles.update');

Route::delete('/pasteles/{pastel}', [PastelController::class, 'destroy'])->name('pasteles.destroy');


🔄 Versión más corta con Route::resource()

php

use App\Http\Controllers\PastelController;


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

¡Hace exactamente lo mismo

Comando para ver todas las rutas en la terminal

bash

php artisan route:list











Creación de vistas

Layout principal (resources/views/layouts/app.blade.php)

<!DOCTYPE html>

<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>

    <meta charset="utf-8">

    <meta name="viewport" content="width=device-width, initial-scale=1">

    <meta name="csrf-token" content="{{ csrf_token() }}">

   

    <title>{{ config('app.name', 'Pastelería') }} - @yield('title', 'Panel de Control')</title>

   

    <!-- Bootstrap 5 CSS -->

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

   

    <!-- Font Awesome Icons -->

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

   

    <!-- DataTables CSS -->

    <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">

   

    @stack('styles')

</head>

<body>




    <!-- Contenido principal -->

    <main class="py-4">

        <div class="container">

            @yield('content')

        </div>

    </main>


    <!-- Scripts -->

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

    <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>

    <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>

   

    @stack('scripts')

</body>

</html>


Vista Index (resources/views/pasteles/index.blade.php)

@extends('layouts.app')


@section('title', 'Lista de Pasteles')


@section('content')

<div class="d-flex justify-content-between align-items-center mb-4">

    <h1><i class="fas fa-cake"></i> Lista de Pasteles</h1>

 


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

                   

                </tbody>

            </table>

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


Comentarios

Entradas más populares de este blog

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

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

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