Foreign keys — foreignId(), constrained(), onDelete()
Concept
The down() method in a migration should be the exact inverse of up(). Writing it correctly enables safe rollbacks. But in production, rollback strategies require careful thought — some changes are irreversible or dangerous to reverse.
What down() must do: Restore the database to the state it was in before up() ran. If up() creates a table, down() drops it. If up() adds a column, down() drops that column.
Why down() is often wrong:
- Dropping a column in
down()that contains production data loses that data permanently. - Rolling back a migration that removed a column would need to recreate it (possibly empty).
- After deploying to production, rollback may be impossible without data loss.
"No down" philosophy: Some teams only use migrate:rollback in development and never in production. For production fixes, they write a NEW migration that corrects the issue. If down() is never safe to run, leave it empty or throw \RuntimeException('This migration cannot be rolled back safely').
Safe down() practices:
- Never have
down()DROP data that doesn't have a backup. - For column removals in
up()— backup data first, thendown()re-adds an empty column. - Always use
Schema::dropIfExists()instead ofSchema::drop()indown(). - For complex
up()operations, consider omittingdown()and adding a comment explaining why.
Schema::table() in down(): When up() adds columns, down() should drop only those specific columns — not the whole table.
Code Example
<?php
// Good: Simple additive migration — down() is safe
return new class extends \Illuminate\Database\Migrations\Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('timezone', 50)->nullable()->after('email');
$table->boolean('newsletter')->default(false)->after('timezone');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['timezone', 'newsletter']);
});
}
};
// Be careful: down() that would lose data
return new class extends \Illuminate\Database\Migrations\Migration
{
public function up(): void
{
// Splits 'full_name' into 'first_name' + 'last_name'
Schema::table('users', function (Blueprint $table) {
$table->string('first_name')->nullable()->after('id');
$table->string('last_name')->nullable()->after('first_name');
});
// Data migration: populate new columns from old
DB::table('users')->whereNotNull('full_name')->each(function($user) {
$parts = explode(' ', $user->full_name, 2);
DB::table('users')->where('id', $user->id)->update([
'first_name' => $parts[0],
'last_name' => $parts[1] ?? '',
]);
});
}
public function down(): void
{
// Re-combining data is possible, but column drop loses new data if run post-populate
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['first_name', 'last_name']);
});
// NOTE: 'full_name' still exists — this down() is relatively safe here
}
};
// No-rollback migration — explicitly not reversible
return new class extends \Illuminate\Database\Migrations\Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('legacy_reference_number');
});
}
public function down(): void
{
throw new \RuntimeException('This migration drops a column and cannot be safely rolled back.');
}
};