Step-by-step
-
1
Understand Migrations vs Seeders
The rule is simple: if it changes the shape of the database (schema), it is a migration. If it changes the contents of the database (rows), it is a seeder. A migration that inserts rows is an antipattern — it ties schema changes to specific data, making rollbacks painful and test environments inconsistent.
-
2
Generate a Migration
Use
make:migrationwith a descriptive name that explains what changes. Laravel infers the table name from the name if you follow the convention. For adding columns to an existing table use theadd_..._to_..._tablepattern.bash# New table php artisan make:migration create_orders_table # Add column to existing table php artisan make:migration add_status_to_orders_table # The file is created in database/migrations/ with a timestamp prefix -
3
Write the up() and down() Methods
The
up()method applies the change. Thedown()method reverses it exactly. A migration without a properdown()is a trap — it blocksmigrate:rollbackin development and makes local environment resets painful. Every change inup()must have a matching reversal indown().php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::table('orders', function (Blueprint $table) { $table->string('status', 20)->default('pending')->after('total'); $table->timestamp('shipped_at')->nullable()->after('status'); $table->index('status'); // Add index for frequent WHERE clauses }); } public function down(): void { Schema::table('orders', function (Blueprint $table) { $table->dropIndex(['status']); $table->dropColumn(['status', 'shipped_at']); }); } }; -
4
Preview SQL Before Running
Use
--pretendto see exactly what SQL will execute without touching the database. This is invaluable before running a migration on production — verify there are no DROP statements you did not expect, and that the SQL syntax looks correct for your database version.bashphp artisan migrate --pretend # Sample output: # CreateOrdersTable: create table `orders` (`id` bigint unsigned not null auto_increment primary key ...) # AddStatusToOrdersTable: alter table `orders` add `status` varchar(20) not null default 'pending' -
5
Run Migrations
Run pending migrations with
php artisan migrate. Laravel tracks which migrations have run in themigrationstable — it will only run files that are not already recorded there. Usemigrate:statusto see what has and has not run.bash# Run all pending migrations php artisan migrate # Check migration status php artisan migrate:status # Rollback the last batch php artisan migrate:rollback # Rollback and re-run all (development only — destroys data) php artisan migrate:fresh -
6
Write an Idempotent Seeder
A seeder should be safe to run multiple times. If you run it twice and it inserts duplicate rows, you have a broken seeder. Use
updateOrCreate()orfirstOrCreate()so the seeder either inserts a new record or updates the existing one — same result every time.php<?php namespace Database\Seeders; use App\Models\Product; use Illuminate\Database\Seeder; class ProductsSeeder extends Seeder { public function run(): void { $products = [ ['sku' => 'PROD-001', 'name' => 'Widget A', 'price' => 9.99], ['sku' => 'PROD-002', 'name' => 'Widget B', 'price' => 14.99], ]; foreach ($products as $data) { Product::updateOrCreate( ['sku' => $data['sku']], // Find by this key $data // Update or insert with these values ); } } } -
7
Register and Run Seeders
Call individual seeders from
DatabaseSeederto establish a dependency order. Run a single seeder in isolation with--class— useful for seeding one table without touching the rest.php<?php namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { // Order matters — seed parents before children $this->call([ CategorySeeder::class, ProductsSeeder::class, UserSeeder::class, ]); } } // Run from command line: // php artisan db:seed — runs DatabaseSeeder // php artisan db:seed --class=ProductsSeeder — runs one seeder -
8
Deploy Safely to Production
In production always use
--force— without it Artisan refuses to run in a non-interactive environment. Never runmigrate:freshormigrate:reseton production; they wipe all data. Back up the database before any migration that drops columns or tables.bash# Standard production deploy php artisan migrate --force # Run a specific seeder on production php artisan db:seed --class=ProductsSeeder --force # What to NEVER do on production: # php artisan migrate:fresh --seed # Drops all tables # php artisan migrate:reset # Rolls back everything
Tips & gotchas
- Name migrations after the <em>change</em>, not the date: <code>add_status_to_orders_table</code> tells you what it does; <code>2024_01_15_142310</code> tells you nothing at 3 AM during an incident.
- If you need to insert reference data (countries, currencies, permission names), seeders are fine — just make them idempotent with <code>updateOrCreate()</code>.
- Never edit a migration that has already been run on production. Create a new migration instead. Editing a ran migration breaks the checksum and confuses <code>migrate:status</code>.
- Use <code>$table->after('column_name')</code> in MySQL to place new columns in a logical position — not required, but makes the schema easier to read.
- Before dropping a column in production, first deploy a migration that makes it nullable (so old code still works), then in a second deploy remove the column and the references to it in code.
Wrapping up
Clean migrations and idempotent seeders are the foundation of a database you can reason about. Follow the schema/data separation, always write a down(), and test with --pretend before going to production.