Schema Changes Should Be Boring
No surprises. No downtime. No data loss.
Basic Migrations
// Create table
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('title');
$table->text('content');
$table->timestamps();
});
// Modify table
Schema::table('posts', function (Blueprint $table) {
$table->string('slug')->after('title');
$table->index('slug');
});
Always Reversible
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone');
});
}
Safe Column Changes
Adding columns is safe. Modifying requires care:
// Safe - new nullable column
$table->string('nickname')->nullable();
// Careful - changing type
$table->string('age')->change(); // Was integer, might fail
// Careful - adding NOT NULL
$table->string('required_field'); // Fails if table has data
Data Migrations
Separate schema from data:
// Migration: add column
$table->string('status')->default('pending');
// Seeder or command: populate data
User::whereNull('status')->update(['status' => 'active']);
Golden Rules
- Never edit deployed migrations - Create new ones
- Test on staging first - Always
- Small incremental changes - Easier to debug
- Backup before migrating - In production
Zero-Downtime Pattern
For large tables:
// Step 1: Add nullable column
$table->string('new_field')->nullable();
// Deploy, run migration
// Step 2: Populate data (in chunks)
User::chunk(1000, fn($users) => /* update */);
// Step 3: Make non-nullable
$table->string('new_field')->nullable(false)->change();
