Reader Stacks

Laravel Migrations: A Practical Reference for Common Column and Table Changes

Adding a column, changing its type, renaming it, dropping a foreign key, or rolling back a specific migration — the everyday migration operations in one reference.

Laravel Migrations: A Practical Reference for Common Column and Table Changes

Migrations handle nearly every schema change in a Laravel app, and while the individual operations are each simple, remembering the exact syntax for a less-common one (dropping a foreign key, changing a column's type) is where people usually reach for a reference.

Adding a column

Schema::table('users', function (Blueprint $table) {
    $table->string('phone')->nullable()->after('email');
});

Adding a default value to a column

Schema::table('posts', function (Blueprint $table) {
    $table->string('status')->default('draft');
});

Changing a column's data type

Schema::table('products', function (Blueprint $table) {
    $table->decimal('price', 10, 2)->change();
});

Changing a column's type requires the doctrine/dbal package on Laravel versions before 11 — Laravel 11 and later dropped this requirement, running column-change migrations without it.

Renaming a column

Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('name', 'full_name');
});

Deleting a column

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

Setting a default null value explicitly

Schema::table('orders', function (Blueprint $table) {
    $table->timestamp('shipped_at')->nullable()->default(null)->change();
});

Adding or updating an index

Schema::table('users', function (Blueprint $table) {
    $table->index('email');
    // or a composite index
    $table->index(['last_name', 'first_name']);
});

Defining relations (foreign keys) in a migration

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
});

foreignId()->constrained() assumes the related table is the plural of the column name minus _id (user_idusers) — pass a table name explicitly (constrained('authors')) when that convention doesn't hold.

Dropping a foreign key

Schema::table('posts', function (Blueprint $table) {
    $table->dropForeign(['user_id']);
});

Creating or updating an enum column

$table->enum('status', ['draft', 'published', 'archived'])->default('draft');

// changing an existing enum's allowed values later
Schema::table('posts', function (Blueprint $table) {
    $table->enum('status', ['draft', 'published', 'archived', 'scheduled'])->change();
});

Rolling back the most recent migration batch vs. a specific migration

php artisan migrate:rollback

// roll back just the last batch (default), or a specific number of batches
php artisan migrate:rollback --step=1

For rolling back a single specific migration rather than a whole batch, temporarily commenting out or removing the others from that batch, or writing a manual down-migration and running it directly, are the practical workarounds — Artisan's rollback command operates on batches, not individual files.

Running a raw SQL query inside a migration

public function up()
{
    DB::statement('ALTER TABLE posts ADD FULLTEXT search_index (title, body)');
}

Dropping to raw SQL inside a migration is a reasonable escape hatch for database-specific features (like MySQL full-text indexes) the schema builder doesn't have a fluent method for.