Skip to main content

Migration Guidelines

Core Principles

Migrations are a critical part of database management in Laravel applications. Following these guidelines ensures smooth database evolution and prevents common issues.

Key Benefit

Well-structured migrations make your database changes reliable, reversible, and maintainable across environments.

Migration Best Practices

1. Single Responsibility Principle

important

Each migration should perform only one logical operation.

Create users table migration
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}

Common single-responsibility operations include:

  • Creating a new table
  • Adding a column to an existing table
  • Modifying existing data
  • Creating indexes

2. Descriptive Naming

Descriptive migration name
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}

3. Always Define Down Methods

Migration with down method
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('users');
}

4. Separate Different Types of Operations

2025_04_12_000001_create_categories_table.php
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}

5. Handle Timeouts Properly

Laravel doesn't automatically detect timeouts in migrations, which can lead to inconsistent states.

Timeout Risks

If a migration times out, Laravel might mark it as completed even if it didn't finish executing.

Solutions:

  • Optimize large data operations
  • Split large migrations into smaller ones
  • Use chunk processing for large datasets
Chunking large data operations
public function up()
{
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// Process each user
$user->update(['status' => 'active']);
}
});
}

6. Immutability of Executed Migrations

caution

Once a migration has been applied to a production database, it should never be modified.

Instead of modifying an existing migration, create a new one to make additional changes.

7. Hardcode Enums in Migrations

important

Always hardcode enum values directly in migrations instead of referencing enum classes.

This ensures that migrations remain stable and independent of code changes. If an enum class is modified or removed in the future, the migration will still work correctly.

Hardcoded enum values
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled']);
$table->timestamps();
});
}

Why this matters:

  • Migrations are historical records and should be immutable
  • Enum classes may change over time, breaking old migrations
  • Hardcoded values ensure migrations can be run reliably at any point in time

8. Foreign Key Management Strategies

The Two-Phase Approach for Existing Tables

danger

Never introduce a new foreign key to an existing table in the same migration that creates its column. Always deploy it over two distinct migrations to preserve rollback safety and avoid lock escalation.

This approach involves two separate migrations:

  1. Migration 1: Add the Column: The new column (e.g., customer_id) is added but made nullable().
  2. Migration 2: Add the Constraint: The foreign key constraint is applied to the column.
2025_04_12_000003_add_customer_id_to_orders.php
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->unsignedBigInteger('customer_id')->nullable()->after('id');
});
}

public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('customer_id');
});
}

Splitting the change allows for data backfilling between deployments, minimizes downtime, and upholds the single responsibility principle.

Exception: Initial Table Creation

When creating a brand new table, it is safe to define foreign keys within the same migration. MySQL processes the entire CREATE TABLE statement, including constraints, within a single transaction. If any part fails, the entire operation is rolled back.

2025_05_21_000001_create_posts_table.php
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('title');
$table->text('body');
$table->timestamps();

// This is safe because it's part of the initial table creation
$table->foreign('user_id')->references('id')->on('users');
});
}

General Rule for Modifications

For any other scenario involving existing tables, you must break down operations into separate, atomic migrations. Each of the following changes requires its own dedicated migration file:

  • Adding a new foreign key constraint.
  • Renaming a column that will have a foreign key.
  • Changing the data type of a column that will have a foreign key.
Rule of Thumb

If you need to perform multiple schema modifications on a column (e.g., rename, then add index, then add foreign key), create a separate migration for each action. This ensures maximum safety and reversibility.

9. Pre-Constraint Data Cleanup

Critical Step for Live Data

Before adding a foreign key or unique constraint to an existing column with data, you must first run a data cleanup migration. Deploying without this step can lead to catastrophic failures in production.

Assume that the target column contains invalid data. Your cleanup migration must identify and resolve these issues before any constraint is applied.

  • For Foreign Keys: Invalid data means a value in the column that does not correspond to an existing ID in the referenced table (e.g., a customer_id of 999 when no customer with that ID exists).
  • For Unique Keys: Invalid data means duplicate values in the column you intend to make unique.

This issue is especially critical across multiple projects that share a similar database structure but have different data. A migration might succeed in a staging environment with clean data but fail during production deployment where invalid data has accumulated.

Here is a safe, three-step process to add a foreign key to a column that already contains data:

2025_05_22_000001_clean_up_invalid_customer_ids_in_orders.php
public function up()
{
// Find orders where customer_id does not exist in the customers table
// and set them to null. Another strategy could be to delete them.
DB::table('orders')
->whereNotNull('customer_id')
->whereNotExists(function ($query) {
$query->select(DB::raw(1))
->from('customers')
->whereColumn('customers.id', 'orders.customer_id');
})
->update(['customer_id' => null]);
}

public function down()
{
// This operation is typically irreversible, as we don't know the original invalid IDs.
// Document this clearly.
}

Common Migration Patterns

Adding a Column

Adding a column to an existing table
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone_number')->nullable()->after('email');
});
}

public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone_number');
});
}

Renaming a Column

Renaming a column
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('email', 'email_address');
});
}

public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('email_address', 'email');
});
}

Creating an Index

Adding an index
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->index(['user_id', 'created_at']);
});
}

public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex(['user_id', 'created_at']);
});
}

Testing Migrations

Testing migrations
# Reset the database and run all migrations
php artisan migrate:fresh

# Rollback the last batch of migrations
php artisan migrate:rollback

# Rollback all migrations and run them again
php artisan migrate:refresh

Conclusion

Following these migration guidelines will help maintain a clean, reliable database structure throughout your application's lifecycle. Remember that migrations serve as a historical record of your database's evolution, so clarity and precision are essential.