Laravel Migrations & Seeders

·

·

Migrations are PHP classes that describe schema changes. They run in order, are tracked in the migrations table, and let teams evolve databases consistently across machines.

Writing Migrations

Create migrations with php artisan make:migration create_posts_table. Timestamp prefixes determine execution order.

Use Schema::create for new tables and Schema::table for alterations. Always consider indexes and foreign keys for relational integrity.

Step 1: Define columns and indexes

Blueprint methods like $table->id(), $table->string('title'), $table->text('body'), and $table->foreignId('user_id')->constrained()->cascadeOnDelete() express schema declaratively.

Add $table->timestamps() for created_at and updated_at. Use softDeletes() when models use the SoftDeletes trait.

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('title');
    $table->timestamps();
});

Step 2: Modify existing tables

php artisan make:migration add_slug_to_posts_table –table=posts generates a stub with Schema::table. Add $table->string('slug')->unique()->after('title') in up().

Always implement down() to reverse changes. Dropping columns that store production data requires backups and communication with stakeholders.

$table->string('slug')->unique();

Step 3: Foreign keys and pivots

Many-to-many tables use post_tag naming alphabetically with post_id and tag_id foreign keys. $table->primary(['post_id', 'tag_id']) prevents duplicate pairs.

Name constraints explicitly when MySQL truncates long names: foreign('user_id')->references('id')->on('users')->name('posts_user_id_foreign').

Schema::create('post_tag', function (Blueprint $table) {
    $table->foreignId('post_id')->constrained();
    $table->foreignId('tag_id')->constrained();
    $table->primary(['post_id', 'tag_id']);
});

Step 4: Run and rollback migrations

php artisan migrate runs pending files. php artisan migrate:rollback steps back one batch. migrate:fresh drops all tables and re-runs—destructive, local only.

Never edit migrations already deployed to production. Create a new migration to adjust schema so teammates and CI share the same history.

php artisan migrate
php artisan migrate:rollback --step=1

Seeders and Factories

Seeders populate tables with initial or demo data. DatabaseSeeder calls other seeders in a defined order respecting foreign key dependencies.

Factories generate fake attributes and integrate with seeders for realistic local datasets.

Step 1: Create and call seeders

php artisan make:seeder PostSeeder and insert data in run() with Post::factory()->count(20)->create(). Call $this->call(PostSeeder::class) from DatabaseSeeder.

Use firstOrCreate for idempotent seeds like admin users so php artisan db:seed does not duplicate rows on every run.

User::firstOrCreate(['email' => 'admin@example.com'], ['name' => 'Admin', 'password' => bcrypt('secret')]);

Step 2: Factory relationships

Post::factory()->for(User::factory())->create() builds related models automatically. Use has() for one-to-many: User::factory()->has(Post::factory()->count(3))->create().

Factory callbacks afterCreating() attach media or permissions once the parent model exists.

User::factory()->has(Post::factory()->count(5))->create();

Step 3: Environment-aware seeding

if (app()->environment('local')) { $this->call(DemoDataSeeder::class); } prevents demo content from loading in production.

Staging environments can use anonymized production snapshots instead of factories when you need realistic volume for performance testing.

if (! app()->isProduction()) { $this->call(DemoSeeder::class); }

Step 4: migrate:fresh –seed workflow

Developers often run php artisan migrate:fresh –seed to reset local databases with a known baseline. Document this in README so onboarding stays smooth.

Pair fresh migrations with automated tests that use RefreshDatabase trait to wrap each test in a transaction or in-memory SQLite.

php artisan migrate:fresh --seed

Production Migration Practices

Production migrations should be backward compatible when zero-downtime deploys matter. Add columns as nullable first, backfill, then enforce NOT NULL in a follow-up migration.

Monitor long-running migrations on large tables; use chunk updates and online schema tools when supported by your database engine.

Step 1: Deployment ordering

Run php artisan migrate –force in CI/CD after code deploys but before traffic shifts. The –force flag skips confirmation in non-interactive shells.

Keep migrations fast. Creating indexes on huge tables may lock writes; use algorithm=inplace options on MySQL 8 or run during maintenance windows.

php artisan migrate --force

Step 2: Schema dumps for speed

php artisan schema:dump prunes old migration files into database/schema/mysql-schema.sql while keeping recent migrations. New environments load the dump then run newer migrations.

Schema dumps help large teams where hundreds of historical migrations slow CI. Commit the dump file and document the workflow.

php artisan schema:dump --prune

Step 3: Testing migrations

Write tests that assert columns exist using Schema::hasColumn or migrate specific paths in feature tests. Rollback tests catch missing down() implementations.

Use separate databases for parallel test workers in PHPUnit 10+ to avoid migration race conditions.

$this->assertTrue(Schema::hasColumn('posts', 'slug'));

Step 4: Data migrations in PHP

Sometimes you migrate data, not just schema. Use DB::table('posts')->whereNull('slug')->orderBy('id')->chunkById(100, function ($rows) { … }) to backfill safely.

Heavy data migrations belong in Artisan commands run once during deploy rather than in synchronous migration classes that timeout in cloud shells.

DB::table('posts')->whereNull('slug')->chunkById(100, function ($rows) {
    foreach ($rows as $row) { ... }
});

Aadyanex ships Laravel schema changes with reviewed migrations, staging parity, and rollback playbooks so production databases evolve without downtime surprises.