6: Migraciones y Seeders en Laravel 12 - Guía Práctica

 

Introducción

Las migraciones son el sistema de control de versiones para tu base de datos en Laravel. Te permiten definir y compartir la estructura de tu base de datos entre todos los miembros del equipo, manteniendo un historial de cambios. Los seeders, por su parte, te permiten poblar tu base de datos con datos de prueba. En este tutorial, aprenderás todo sobre migraciones y seeders en Laravel 12.

Índice

  1. ¿Qué son las Migraciones?

  2. Creación de Migraciones

  3. Estructura de una Migración

  4. Tipos de Datos en Migraciones

  5. Modificaciones de Tablas

  6. Comandos de Migración

  7. ¿Qué son los Seeders?

  8. Creación de Seeders

  9. Uso de Faker

  10. DatabaseSeeder y Orden de Ejecución

  11. Ejercicio Práctico: Sistema de Pastelería

  12. Buenas Prácticas


1. ¿Qué son las Migraciones?

Las migraciones son archivos PHP que definen cambios en la estructura de tu base de datos. Funcionan como un sistema de control de versiones para tu esquema de base de datos.

Beneficios de las Migraciones

  • Control de versiones: Cada cambio queda registrado en un archivo

  • Portabilidad: Funciona con diferentes sistemas de bases de datos (MySQL, PostgreSQL, SQLite, etc.)

  • Colaboración en equipo: Todos los miembros pueden mantener su base de datos sincronizada

  • Rollback: Puedes deshacer cambios fácilmente

  • Historial: Puedes ver todos los cambios realizados

Ejemplo de estructura

text
database/
├── migrations/
│   ├── 2014_10_12_000000_create_users_table.php
│   ├── 2014_10_12_100000_create_password_reset_tokens_table.php
│   ├── 2024_01_01_000000_create_pasteles_table.php
│   ├── 2024_01_02_000000_add_precio_to_pasteles_table.php
│   └── 2024_01_03_000000_create_ventas_table.php
└── seeders/
    ├── DatabaseSeeder.php
    ├── PastelSeeder.php
    └── VentaSeeder.php

2. Creación de Migraciones

Comandos básicos

bash
# Crear una migración básica
php artisan make:migration nombre_migracion

# Crear migración con plantilla para tabla
php artisan make:migration nombre_migracion --create=nombre_tabla

# Crear migración para modificar tabla existente
php artisan make:migration nombre_migracion --table=nombre_tabla

# Crear migración con modelo y factory
php artisan make:migration nombre_migracion --create=nombre_tabla --model=NombreModelo

Ejemplos prácticos

bash
# Crear tabla pasteles
php artisan make:migration crear_tabla_pasteles --create=pasteles

# Crear tabla categorías
php artisan make:migration crear_tabla_categorias --create=categorias

# Agregar campos a tabla existente
php artisan make:migration agregar_campos_a_pasteles --table=pasteles

# Crear tabla con modelo
php artisan make:migration crear_tabla_ingredientes --create=ingredientes --model=Ingrediente

3. Estructura de una Migración

Migración básica para crear tabla

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();
            $table->string('nombre', 100);
            $table->enum('sabor', ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']);
            $table->decimal('precio', 8, 2)->nullable();
            $table->boolean('disponible')->default(true);
            $table->text('descripcion')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('pasteles');
    }
};

Migración para modificar tabla

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::table('pasteles', function (Blueprint $table) {
            $table->string('codigo', 20)->unique()->after('id');
            $table->integer('stock')->default(0)->after('precio');
            $table->dropColumn('descripcion');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('pasteles', function (Blueprint $table) {
            $table->dropColumn(['codigo', 'stock']);
            $table->text('descripcion')->nullable();
        });
    }
};

4. Tipos de Datos en Migraciones

Tipos de datos principales

php
// En la función up()
Schema::create('tabla', function (Blueprint $table) {
    // Tipos básicos
    $table->id();                       // BIGINT UNSIGNED auto-increment
    $table->bigIncrements('id');        // BIGINT UNSIGNED auto-increment
    $table->increments('id');           // INT UNSIGNED auto-increment
    $table->integer('numero');          // INT
    $table->bigInteger('grande');       // BIGINT
    $table->string('texto', 100);       // VARCHAR(100)
    $table->text('descripcion');        // TEXT
    $table->longText('contenido');      // LONGTEXT
    $table->decimal('precio', 8, 2);    // DECIMAL(8,2)
    $table->float('porcentaje');        // FLOAT
    $table->double('valor');            // DOUBLE
    $table->boolean('activo');          // BOOLEAN
    $table->date('fecha');              // DATE
    $table->dateTime('fecha_hora');     // DATETIME
    $table->timestamp('creado_en');     // TIMESTAMP
    $table->time('hora');               // TIME
    $table->enum('estado', ['activo', 'inactivo']); // ENUM
    $table->json('datos');              // JSON
    $table->uuid('uuid');               // UUID

    // Campos especiales
    $table->timestamps();               // created_at, updated_at
    $table->softDeletes();              // deleted_at
    $table->rememberToken();            // remember_token

    // Relaciones
    $table->foreignId('categoria_id')->constrained();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->unsignedBigInteger('pastel_id')->nullable();
});

Modificadores de columnas

php
Schema::create('ejemplo', function (Blueprint $table) {
    $table->id();
    $table->string('nombre')->nullable();              // Puede ser NULL
    $table->string('apellido')->default('Sin apellido'); // Valor por defecto
    $table->string('email')->unique();                 // Único
    $table->integer('edad')->unsigned();               // Sin signo
    $table->string('codigo', 10)->after('id');         // Después de id
    $table->boolean('activo')->default(true)->index(); // Índice
    $table->string('slug')->unique()->comment('Slug para URL'); // Comentario
});

5. Modificaciones de Tablas

Agregar columnas

php
// Crear migración para agregar campos
php artisan make:migration agregar_campos_a_pasteles --table=pasteles

// En la migración
Schema::table('pasteles', function (Blueprint $table) {
    $table->string('imagen', 255)->nullable()->after('descripcion');
    $table->integer('stock')->default(0)->after('precio');
    $table->timestamp('fecha_vencimiento')->nullable()->after('stock');
});

Renombrar columnas

php
// Asegúrate de tener el paquete doctrine/dbal instalado
composer require doctrine/dbal

Schema::table('pasteles', function (Blueprint $table) {
    $table->renameColumn('precio', 'precio_unitario');
    $table->renameColumn('disponible', 'en_stock');
});

Cambiar tipo de columna

php
Schema::table('pasteles', function (Blueprint $table) {
    $table->decimal('precio', 10, 2)->change();
    $table->string('nombre', 150)->change();
    $table->text('descripcion')->nullable()->change();
});

Eliminar columnas

php
Schema::table('pasteles', function (Blueprint $table) {
    $table->dropColumn('imagen');
    $table->dropColumn(['stock', 'fecha_vencimiento']);
});

// Eliminar con índices
Schema::table('pasteles', function (Blueprint $table) {
    $table->dropColumn(['nombre', 'email']);
});

Índices en migraciones

php
Schema::create('pasteles', function (Blueprint $table) {
    $table->id();
    $table->string('nombre');
    $table->string('sabor');
    $table->decimal('precio', 8, 2);
    $table->boolean('disponible');
    $table->timestamps();

    // Índices
    $table->index('nombre');                    // Índice simple
    $table->unique('email');                    // Índice único
    $table->index(['nombre', 'sabor']);         // Índice compuesto
    $table->fullText('descripcion');            // Índice de texto completo
});

// Índices en tablas existentes
Schema::table('pasteles', function (Blueprint $table) {
    $table->index('nombre');
    $table->unique('email');
});

// Eliminar índices
Schema::table('pasteles', function (Blueprint $table) {
    $table->dropIndex('pasteles_nombre_index');
    $table->dropUnique('pasteles_email_unique');
});

6. Comandos de Migración

Comandos principales

bash
# Ejecutar migraciones pendientes
php artisan migrate

# Ejecutar migraciones en entorno específico
php artisan migrate --env=testing

# Forzar migración en producción
php artisan migrate --force

# Deshacer última migración
php artisan migrate:rollback

# Deshacer todas las migraciones
php artisan migrate:reset

# Deshacer y volver a ejecutar migraciones
php artisan migrate:refresh

# Deshacer, volver a ejecutar y ejecutar seeders
php artisan migrate:refresh --seed

# Ver estado de las migraciones
php artisan migrate:status

# Instalar migración (crea tabla migrations)
php artisan migrate:install

# Crear una migración nueva
php artisan make:migration nombre_migracion

# Crear migración con tabla
php artisan make:migration nombre_migracion --create=tabla

# Crear migración para modificar
php artisan make:migration nombre_migracion --table=tabla

Ejemplos de uso

bash
# Crear y ejecutar migración para pasteles
php artisan make:migration crear_tabla_pasteles --create=pasteles
php artisan migrate

# Agregar campo y ejecutar
php artisan make:migration agregar_precio_a_pasteles --table=pasteles
php artisan migrate

# Deshacer último cambio
php artisan migrate:rollback

# Ver estado
php artisan migrate:status

# Resetear y empezar de nuevo
php artisan migrate:refresh --seed

# Ejecutar migraciones específicas
php artisan migrate --path=database/migrations/2024_01_01_000000_create_pasteles_table.php

7. ¿Qué son los Seeders?

Los seeders son archivos que te permiten poblar tu base de datos con datos de prueba o iniciales. Son especialmente útiles para:

  • Generar datos de prueba para desarrollo

  • Crear datos iniciales para producción

  • Mantener la consistencia en el equipo de desarrollo

  • Realizar pruebas automatizadas

Estructura de un Seeder

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class PastelSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Código para insertar datos
    }
}

8. Creación de Seeders

Comandos básicos

bash
# Crear un seeder
php artisan make:seeder PastelSeeder

# Crear seeder con modelo y factory
php artisan make:seeder PastelSeeder --model=Pastel

Seeder básico sin Faker

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class PastelSeeder extends Seeder
{
    public function run(): void
    {
        DB::table('pasteles')->insert([
            [
                'nombre' => 'Pastel de Chocolate Clásico',
                'sabor' => 'chocolate',
                'precio' => 250.00,
                'disponible' => true,
                'descripcion' => 'Delicioso pastel de chocolate con cobertura de ganache',
                'created_at' => now(),
                'updated_at' => now(),
            ],
            [
                'nombre' => 'Pastel de Vainilla Especial',
                'sabor' => 'vainilla',
                'precio' => 180.00,
                'disponible' => true,
                'descripcion' => 'Pastel de vainilla con crema de mantequilla',
                'created_at' => now(),
                'updated_at' => now(),
            ],
        ]);
    }
}

Seeder con Faker

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Faker\Factory as Faker;
use Illuminate\Support\Facades\DB;

class PastelSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('es_ES');

        for ($i = 0; $i < 50; $i++) {
            DB::table('pasteles')->insert([
                'nombre' => 'Pastel ' . $faker->firstNameFemale() . ' ' . $faker->lastName(),
                'sabor' => $faker->randomElement(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']),
                'precio' => $faker->randomFloat(2, 50, 500),
                'disponible' => $faker->boolean(80),
                'descripcion' => $faker->sentence(10),
                'created_at' => $faker->dateTimeBetween('-1 year', 'now'),
                'updated_at' => $faker->dateTimeBetween('-1 month', 'now'),
            ]);
        }
    }
}

9. Uso de Faker

Instalación y configuración

bash
# Faker ya viene incluido en Laravel 12
# Verificar en composer.json
composer require fakerphp/faker

Faker básico en Seeder

php
use Faker\Factory as Faker;

public function run(): void
{
    $faker = Faker::create('es_ES'); // Configurar en español

    for ($i = 0; $i < 100; $i++) {
        DB::table('pasteles')->insert([
            'nombre' => 'Pastel ' . $faker->firstName(),
            'sabor' => $faker->randomElement(['chocolate', 'vainilla', 'cheesecake']),
            'precio' => $faker->randomFloat(2, 50, 500),
            'disponible' => $faker->boolean(75),
            'descripcion' => $faker->sentence(8),
            'created_at' => $faker->dateTimeBetween('-6 months', 'now'),
            'updated_at' => $faker->dateTimeBetween('-1 month', 'now'),
        ]);
    }
}

Tipos de datos de Faker

php
// Nombres y personas
$faker->name();                 // Juan Pérez
$faker->firstName();            // María
$faker->lastName();             // García
$faker->firstNameFemale();      // Ana
$faker->firstNameMale();        // Carlos

// Direcciones
$faker->address();              // Calle Principal 123
$faker->city();                 // Madrid
$faker->country();              // España
$faker->postcode();             // 28001

// Teléfonos y emails
$faker->phoneNumber();          // 612345678
$faker->email();                // usuario@email.com
$faker->safeEmail();            // usuario@example.com

// Texto
$faker->sentence();             // Esto es una frase de ejemplo.
$faker->paragraph();            // Párrafo completo de ejemplo.
$faker->text();                 // Texto más largo

// Fechas
$faker->date();                 // 2024-01-15
$faker->dateTime();             // 2024-01-15 14:30:00
$faker->dateTimeBetween('-1 year', 'now'); // Fecha entre año pasado y ahora

// Otros
$faker->randomElement(['a', 'b', 'c']); // Elemento aleatorio
$faker->randomNumber(5);        // Número de 5 dígitos
$faker->boolean();              // true o false
$faker->uuid();                 // UUID
$faker->ipv4();                 // Dirección IPv4

Faker para diferentes tipos de datos

php
class PastelSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('es_ES');

        for ($i = 0; $i < 50; $i++) {
            $sabor = $faker->randomElement(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']);
            $precio = $this->obtenerPrecioPorSabor($sabor, $faker);
            
            DB::table('pasteles')->insert([
                'nombre' => $this->generarNombre($sabor, $faker),
                'sabor' => $sabor,
                'precio' => $precio,
                'disponible' => $faker->boolean(80),
                'descripcion' => $this->generarDescripcion($sabor, $faker),
                'codigo' => 'PST-' . strtoupper($faker->bothify('??####')),
                'stock' => $faker->numberBetween(0, 100),
                'created_at' => $faker->dateTimeBetween('-1 year', 'now'),
                'updated_at' => $faker->dateTimeBetween('-1 month', 'now'),
            ]);
        }
    }

    private function obtenerPrecioPorSabor(string $sabor, $faker): float
    {
        $precios = [
            'chocolate' => [80, 250],
            'vainilla' => [60, 200],
            'cheesecake' => [120, 350],
            'fresa' => [70, 220],
            'coco' => [90, 250]
        ];

        list($min, $max) = $precios[$sabor] ?? [50, 200];
        return $faker->randomFloat(2, $min, $max);
    }

    private function generarNombre(string $sabor, $faker): string
    {
        $adjetivos = ['Delicioso', 'Exquisito', 'Especial', 'Increíble', 'Fabuloso'];
        $sustantivos = ['Pastel', 'Torta', 'Bocado', 'Dulce'];
        
        return $faker->randomElement($adjetivos) . ' ' .
               ucfirst($sabor) . ' ' .
               $faker->randomElement($sustantivos);
    }

    private function generarDescripcion(string $sabor, $faker): string
    {
        $descripciones = [
            'chocolate' => 'Exquisito pastel de chocolate con cobertura de ganache',
            'vainilla' => 'Delicado pastel de vainilla con crema de mantequilla',
            'cheesecake' => 'Cremoso cheesecake con base de galleta y topping de fresa',
            'fresa' => 'Pastel de fresa con relleno de crema y fresas naturales',
            'coco' => 'Pastel de coco con crema de coco y coco rallado'
        ];
        
        return $descripciones[$sabor] ?? $faker->sentence(10);
    }
}

10. DatabaseSeeder y Orden de Ejecución

DatabaseSeeder básico

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            CategoriaSeeder::class,
            PastelSeeder::class,
            IngredienteSeeder::class,
            VentaSeeder::class,
        ]);
    }
}

Orden de ejecución con relaciones

php
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Primero categorías (sin dependencias)
        $this->call(CategoriaSeeder::class);
        
        // Luego pasteles (dependen de categorías)
        $this->call(PastelSeeder::class);
        
        // Luego ingredientes (dependen de pasteles)
        $this->call(IngredienteSeeder::class);
        
        // Finalmente ventas (dependen de pasteles)
        $this->call(VentaSeeder::class);
    }
}

Ejecutar seeders con opciones

bash
# Ejecutar todos los seeders
php artisan db:seed

# Ejecutar un seeder específico
php artisan db:seed --class=PastelSeeder

# Ejecutar en entorno de testing
php artisan db:seed --env=testing

# Forzar en producción
php artisan db:seed --force

# Combinar con migración
php artisan migrate:fresh --seed
php artisan migrate:refresh --seed

11. Ejercicio Práctico: Sistema de Pastelería

Paso 1: Crear migraciones

bash
# Crear migraciones para todas las tablas
php artisan make:migration crear_tabla_categorias --create=categorias
php artisan make:migration crear_tabla_pasteles --create=pasteles
php artisan make:migration crear_tabla_ingredientes --create=ingredientes
php artisan make:migration crear_tabla_pastel_ingrediente --create=pastel_ingrediente
php artisan make:migration crear_tabla_ventas --create=ventas

Paso 2: Definir las migraciones

php
// database/migrations/xxxx_create_categorias_table.php
Schema::create('categorias', function (Blueprint $table) {
    $table->id();
    $table->string('nombre', 100);
    $table->text('descripcion')->nullable();
    $table->string('color', 20)->nullable();
    $table->timestamps();
});

// database/migrations/xxxx_create_pasteles_table.php
Schema::create('pasteles', function (Blueprint $table) {
    $table->id();
    $table->string('nombre', 100);
    $table->string('slug')->unique();
    $table->enum('sabor', ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco', 'cafe', 'limon']);
    $table->decimal('precio', 8, 2);
    $table->integer('stock')->default(0);
    $table->boolean('disponible')->default(true);
    $table->text('descripcion')->nullable();
    $table->string('imagen')->nullable();
    $table->foreignId('categoria_id')->constrained()->onDelete('cascade');
    $table->softDeletes();
    $table->timestamps();
    
    $table->index('sabor');
    $table->index('disponible');
});

// database/migrations/xxxx_create_ingredientes_table.php
Schema::create('ingredientes', function (Blueprint $table) {
    $table->id();
    $table->string('nombre', 100);
    $table->decimal('stock', 8, 2)->default(0);
    $table->string('unidad', 20)->default('g');
    $table->decimal('precio_unitario', 8, 2)->default(0);
    $table->timestamps();
});

// database/migrations/xxxx_create_pastel_ingrediente_table.php
Schema::create('pastel_ingrediente', function (Blueprint $table) {
    $table->id();
    $table->foreignId('pastel_id')->constrained()->onDelete('cascade');
    $table->foreignId('ingrediente_id')->constrained()->onDelete('cascade');
    $table->decimal('cantidad', 8, 2);
    $table->string('unidad', 20);
    $table->timestamps();
});

// database/migrations/xxxx_create_ventas_table.php
Schema::create('ventas', function (Blueprint $table) {
    $table->id();
    $table->foreignId('pastel_id')->constrained()->onDelete('cascade');
    $table->integer('cantidad');
    $table->decimal('precio_unitario', 8, 2);
    $table->decimal('subtotal', 10, 2);
    $table->decimal('iva', 10, 2);
    $table->decimal('total', 10, 2);
    $table->string('cliente_nombre', 100);
    $table->string('cliente_email', 100)->nullable();
    $table->string('cliente_telefono', 20)->nullable();
    $table->enum('estado', ['pendiente', 'pagado', 'entregado', 'cancelado'])->default('pendiente');
    $table->timestamp('fecha_venta');
    $table->timestamps();
    
    $table->index('estado');
    $table->index('fecha_venta');
});

Paso 3: Crear seeders

bash
php artisan make:seeder CategoriaSeeder
php artisan make:seeder PastelSeeder
php artisan make:seeder IngredienteSeeder
php artisan make:seeder VentaSeeder

Paso 4: Implementar seeders

php
// database/seeders/CategoriaSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class CategoriaSeeder extends Seeder
{
    public function run(): void
    {
        $categorias = [
            ['nombre' => 'Tortas Clásicas', 'descripcion' => 'Tortas tradicionales', 'color' => '#FF6B6B'],
            ['nombre' => 'Pasteles Especiales', 'descripcion' => 'Pasteles para ocasiones especiales', 'color' => '#4ECDC4'],
            ['nombre' => 'Repostería Fina', 'descripcion' => 'Repostería de alta calidad', 'color' => '#45B7D1'],
            ['nombre' => 'Sin Gluten', 'descripcion' => 'Pasteles libres de gluten', 'color' => '#96CEB4'],
            ['nombre' => 'Veganos', 'descripcion' => 'Pasteles 100% vegetales', 'color' => '#FFEAA7'],
        ];

        foreach ($categorias as $categoria) {
            DB::table('categorias')->insert([
                'nombre' => $categoria['nombre'],
                'descripcion' => $categoria['descripcion'],
                'color' => $categoria['color'],
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }
}

// database/seeders/PastelSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Faker\Factory as Faker;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class PastelSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('es_ES');
        $categorias = DB::table('categorias')->pluck('id')->toArray();
        
        $sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco', 'cafe', 'limon'];
        $descripciones = [
            'chocolate' => 'Exquisito pastel de chocolate con cobertura de ganache y decoración de chocolate',
            'vainilla' => 'Delicado pastel de vainilla con crema de mantequilla y frutos rojos',
            'cheesecake' => 'Cremoso cheesecake con base de galleta y topping de fresa',
            'fresa' => 'Pastel de fresa con relleno de crema y fresas naturales',
            'coco' => 'Pastel de coco con crema de coco y coco rallado',
            'cafe' => 'Pastel de café con crema de mantequilla y nueces',
            'limon' => 'Pastel de limón con merengue y ralladura de limón'
        ];

        for ($i = 0; $i < 30; $i++) {
            $sabor = $faker->randomElement($sabores);
            $precio = $this->obtenerPrecioPorSabor($sabor);
            
            DB::table('pasteles')->insert([
                'nombre' => $this->generarNombre($sabor, $faker),
                'slug' => Str::slug('pastel-' . $faker->unique()->firstName() . '-' . $sabor),
                'sabor' => $sabor,
                'precio' => $precio,
                'stock' => $faker->numberBetween(0, 50),
                'disponible' => $faker->boolean(80),
                'descripcion' => $descripciones[$sabor] . '. ' . $faker->sentence(5),
                'imagen' => 'pasteles/' . $sabor . '_' . $faker->numberBetween(1, 5) . '.jpg',
                'categoria_id' => $faker->randomElement($categorias),
                'created_at' => $faker->dateTimeBetween('-6 months', 'now'),
                'updated_at' => $faker->dateTimeBetween('-1 month', 'now'),
            ]);
        }
    }

    private function obtenerPrecioPorSabor(string $sabor): float
    {
        $precios = [
            'chocolate' => [80, 250],
            'vainilla' => [60, 200],
            'cheesecake' => [120, 350],
            'fresa' => [70, 220],
            'coco' => [90, 250],
            'cafe' => [80, 230],
            'limon' => [60, 180]
        ];

        list($min, $max) = $precios[$sabor] ?? [50, 200];
        return round(rand($min * 100, $max * 100) / 100, 2);
    }

    private function generarNombre(string $sabor, $faker): string
    {
        $adjetivos = ['Delicioso', 'Exquisito', 'Especial', 'Increíble', 'Fabuloso', 'Sensacional'];
        return $faker->randomElement($adjetivos) . ' ' . ucfirst($sabor) . ' ' . $faker->randomElement(['Pastel', 'Torta', 'Dulce']);
    }
}

// database/seeders/IngredienteSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Faker\Factory as Faker;
use Illuminate\Support\Facades\DB;

class IngredienteSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('es_ES');
        $ingredientes = ['Harina', 'Azúcar', 'Huevo', 'Mantequilla', 'Chocolate', 'Vainilla', 'Fresa', 'Coco', 'Café', 'Limón'];
        
        foreach ($ingredientes as $ingrediente) {
            DB::table('ingredientes')->insert([
                'nombre' => $ingrediente,
                'stock' => $faker->randomFloat(2, 10, 1000),
                'unidad' => $faker->randomElement(['g', 'kg', 'ml', 'L', 'unidad']),
                'precio_unitario' => $faker->randomFloat(2, 1, 50),
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }
}

// database/seeders/VentaSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Faker\Factory as Faker;
use Illuminate\Support\Facades\DB;

class VentaSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('es_ES');
        $pasteles = DB::table('pasteles')->pluck('id', 'precio')->toArray();
        $estados = ['pendiente', 'pagado', 'entregado', 'cancelado'];

        for ($i = 0; $i < 50; $i++) {
            $pastelId = $faker->randomElement(array_keys($pasteles));
            $precioUnitario = $pasteles[$pastelId];
            $cantidad = $faker->numberBetween(1, 5);
            $subtotal = $precioUnitario * $cantidad;
            $iva = $subtotal * 0.16;
            $total = $subtotal + $iva;
            $estado = $faker->randomElement($estados);
            
            DB::table('ventas')->insert([
                'pastel_id' => $pastelId,
                'cantidad' => $cantidad,
                'precio_unitario' => $precioUnitario,
                'subtotal' => $subtotal,
                'iva' => $iva,
                'total' => $total,
                'cliente_nombre' => $faker->name(),
                'cliente_email' => $faker->email(),
                'cliente_telefono' => $faker->phoneNumber(),
                'estado' => $estado,
                'fecha_venta' => $faker->dateTimeBetween('-3 months', 'now'),
                'created_at' => $faker->dateTimeBetween('-3 months', 'now'),
                'updated_at' => $faker->dateTimeBetween('-1 month', 'now'),
            ]);
        }
    }
}

Paso 5: Configurar DatabaseSeeder

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            CategoriaSeeder::class,
            IngredienteSeeder::class,
            PastelSeeder::class,
            VentaSeeder::class,
        ]);
    }
}

Paso 6: Ejecutar migraciones y seeders

bash
# Crear las tablas y poblar con datos
php artisan migrate:fresh --seed

# O paso a paso
php artisan migrate
php artisan db:seed

# Verificar en la base de datos
# Deberías tener datos en las tablas: categorias, pasteles, ingredientes, pastel_ingrediente, ventas

12. Buenas Prácticas

1. Usar convenciones de nombres

php
// ✅ Buenas prácticas
Schema::create('pasteles', function (Blueprint $table) { ... });
$table->foreignId('categoria_id')->constrained();

// ❌ Malas prácticas
Schema::create('pastel', function (Blueprint $table) { ... });
$table->integer('cat_id');

2. Mantener migraciones ordenadas

php
// Crear migraciones por módulos
php artisan make:migration crear_tabla_pasteles --create=pasteles
php artisan make:migration crear_tabla_categorias --create=categorias
php artisan make:migration crear_tabla_relaciones_pasteles_categorias

3. Usar timestamps siempre

php
// ✅ Siempre incluir timestamps
$table->timestamps();
$table->softDeletes();

// ❌ No hacer
// omitir timestamps

4. Índices para rendimiento

php
$table->index('sabor');
$table->index(['categoria_id', 'disponible']);
$table->unique('codigo');

5. Seeders con datos realistas

php
// ✅ Usar Faker para datos realistas
$faker = Faker::create('es_ES');

// ❌ Datos genéricos y repetitivos
DB::table('pasteles')->insert([
    ['nombre' => 'Pastel 1'],
    ['nombre' => 'Pastel 2'],
]);

6. Mantener DatabaseSeeder limpio

php
// ✅ Llamar a otros seeders
$thiscall([
    CategoriaSeeder::class,
    PastelSeeder::class,
]);

// ❌ No meter toda la lógica aquí

7. Rollback y control de versiones

php
// ✅ Siempre implementar down()
public function down(): void
{
    Schema::dropIfExists('pasteles');
}

// ❌ No implementar down()
// Sin down no se puede hacer rollback

8. Verificar el estado de migraciones

bash
# Antes de hacer cambios importantes
php artisan migrate:status

# Siempre hacer backup antes de migrar en producción
# Usar migraciones con --pretend para ver qué va a ejecutar
php artisan migrate --pretend

9. Separar datos de prueba de los datos reales

php
// En desarrollo: datos realistas
// En producción: datos mínimos necesarios
if (app()->environment('local')) {
    $this->call(TestingSeeder::class);
} else {
    $this->call(ProductionSeeder::class);
}

Comandos Útiles

bash
# Crear migración
php artisan make:migration nombre_migracion --create=tabla

# Ejecutar migraciones
php artisan migrate

# Deshacer última migración
php artisan migrate:rollback

# Ver estado
php artisan migrate:status

# Crear seeder
php artisan make:seeder NombreSeeder

# Ejecutar seeders
php artisan db:seed

# Ejecutar todo (migración fresca + seeder)
php artisan migrate:fresh --seed

# En producción
php artisan migrate --force

Resumen

ConceptoDescripciónComando/Archivo
MigraciónDefine estructura de tabladatabase/migrations/
SeederPuebla datos de pruebadatabase/seeders/
FakerGenera datos realistasFaker\Factory
DatabaseSeederOrquesta todos los seederscall([...])
RollbackDeshace migracionesmigrate:rollback
RefreshResetea migracionesmigrate:refresh

¡Con las migraciones y seeders tienes un control completo sobre la estructura y datos de tu base de datos en Laravel 12

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