Reader Stacks

Laravel Migrations: Adding Columns, Defaults, Indexes, and Enums

Four common schema changes on an existing table — adding a column, giving it a default, indexing it, and adding an enum column — plus the one package they all quietly depend on for SQLite.

Once a table already exists, changing it — rather than defining it fresh — uses Schema::table() instead of Schema::create(). The operations below are the ones that come up most often once an app is past its first migration.

1. Adding a column

php artisan make:migration add_phone_to_users_table --table=users
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('phone')->nullable()->after('email');
    });
}

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

A new column added to a table that already has rows should almost always be ->nullable() or carry a ->default(...) — a NOT NULL column with no default fails the migration outright on any table that isn't empty, because MySQL has no value to put in the new column for existing rows.

2. Adding a default value

$table->string('status')->default('pending');
$table->boolean('is_featured')->default(false);
$table->unsignedInteger('view_count')->default(0);

The default applies at the database level, not just in application code — any row inserted without explicitly setting that column (including rows inserted by direct SQL, a raw query, or another service entirely) gets the default automatically, which is a stronger guarantee than only setting a default in an Eloquent model.

3. Adding an index

Schema::table('orders', function (Blueprint $table) {
    $table->index('customer_id');
    $table->unique('order_number');
    $table->index(['status', 'created_at']); // composite index
});

An index speeds up lookups and WHERE/ORDER BY on that column, at the cost of slightly slower writes and extra storage — worth adding on any column regularly used to filter or sort, not worth adding on every column by default. A composite index like ['status', 'created_at'] is most useful specifically when queries filter and sort by both columns together in that order.

4. Adding an enum column

Schema::table('orders', function (Blueprint $table) {
    $table->enum('status', ['pending', 'processing', 'shipped', 'cancelled'])
        ->default('pending');
});

A database-level enum restricts the column to exactly those values at the schema layer — an insert or update with any other value fails at the database, not just at the application's validation layer. The trade-off: adding a new valid value later means writing another migration to alter the column, which is more friction than a plain string column paired with application-level validation. For a value set that's genuinely fixed and rarely changes (order status, user role), that trade-off is usually worth it; for anything likely to grow, a plain string column (or a separate lookup table) ages better.

5. Modifying an existing column's type

Schema::table('products', function (Blueprint $table) {
    $table->string('name', 500)->change();
});

->change() requires the doctrine/dbal package to be installed (composer require doctrine/dbal) on Laravel versions before 11 — without it, calling ->change() throws an error saying the driver doesn't support altering columns. Laravel 11+ removed this dependency and supports column modification natively.

Topics: Database Migrations