Introduction
Schemas evolve. Laravel's Schema Builder supports adding, modifying, dropping, and renaming columns via follow-up migrations — no doctrine/dbal required since Laravel 11.
Key Concepts
Schema::table: The method used to modify an existing table (vs.Schema::createfor new ones).->change(): A modifier that turns a column definition into an ALTER statement for an existing column.- Dropping columns and indexes:
dropColumn,dropIndex,dropUnique,dropForeign— each with a specific naming convention. - Renaming:
renameColumnfor columns,Schema::renamefor tables. - Existence checks:
Schema::hasTableandSchema::hasColumnfor defensive migrations.
Real World Context
Once a feature is live in production, every schema change happens through a new migration — not by editing the old one. This lesson is the toolkit for those forward-only evolutions: new columns, type changes, cleanups, and renames.
Deep Dive
As your application evolves, you'll need to modify existing database tables. Laravel makes this easy with migration commands for adding, modifying, and removing columns.
Adding Columns
bashphp artisan make:migration add_phone_to_users_table --table=users
phppublic function up(): void { Schema::table('users', function (Blueprint $table) { $table->string('phone', 20)->nullable()->after('email'); $table->date('birth_date')->nullable(); $table->json('preferences')->nullable(); }); } public function down(): void { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['phone', 'birth_date', 'preferences']); }); }
Modifying Columns
Since Laravel 11, column modifications no longer require the doctrine/dbal package — Laravel's native schema builder handles them directly across MySQL, PostgreSQL, SQLite, and SQL Server. Just call ->change() on any column definition:
phppublic function up(): void { Schema::table('posts', function (Blueprint $table) { // Change column type $table->text('title')->change(); // Was string, now text // Make nullable $table->string('subtitle')->nullable()->change(); // Change default $table->integer('views')->default(0)->change(); // Rename column $table->renameColumn('body', 'content'); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { $table->string('title', 255)->change(); $table->string('subtitle')->nullable(false)->change(); $table->integer('views')->default(0)->change(); $table->renameColumn('content', 'body'); }); }
Dropping Columns
phppublic function up(): void { Schema::table('users', function (Blueprint $table) { // Drop single column $table->dropColumn('legacy_field'); // Drop multiple columns $table->dropColumn(['old_field', 'unused_field']); }); }
Dropping with Indexes/Foreign Keys
phppublic function up(): void { Schema::table('posts', function (Blueprint $table) { // Drop foreign key first, then column $table->dropForeign(['user_id']); // Drops posts_user_id_foreign $table->dropColumn('user_id'); // Drop index then column $table->dropIndex(['status']); // Drops posts_status_index $table->dropColumn('status'); // Drop unique constraint $table->dropUnique(['email']); // Drops posts_email_unique }); }
Managing Indexes
phppublic function up(): void { Schema::table('posts', function (Blueprint $table) { // Add indexes $table->index('slug'); $table->unique('email'); $table->index(['status', 'published_at'], 'posts_status_published_index'); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { // Drop by column name (Laravel generates name) $table->dropIndex(['slug']); // posts_slug_index $table->dropUnique(['email']); // posts_email_unique // Or drop by explicit name $table->dropIndex('posts_status_published_index'); }); }
Renaming Tables
phppublic function up(): void { Schema::rename('posts', 'articles'); } public function down(): void { Schema::rename('articles', 'posts'); }
Checking Table/Column Existence
phppublic function up(): void { // Check if table exists if (Schema::hasTable('users')) { // Modify table } // Check if column exists if (Schema::hasColumn('users', 'email')) { // Column exists } // Check multiple columns if (Schema::hasColumns('users', ['email', 'name'])) { // Both columns exist } }
Real-World Migration Examples
Adding a Status Column with Enum
phppublic function up(): void { Schema::table('orders', function (Blueprint $table) { $table->enum('status', ['pending', 'processing', 'shipped', 'delivered', 'cancelled']) ->default('pending') ->after('total'); }); }
Converting Nullable to Required
phppublic function up(): void { // First, update existing null values DB::table('posts') ->whereNull('published_at') ->update(['published_at' => now()]); // Then make non-nullable Schema::table('posts', function (Blueprint $table) { $table->timestamp('published_at')->nullable(false)->change(); }); }
Adding Soft Deletes
phppublic function up(): void { Schema::table('posts', function (Blueprint $table) { $table->softDeletes(); // Adds deleted_at column }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { $table->dropSoftDeletes(); // Removes deleted_at column }); }
Pivot Table for Many-to-Many
phppublic function up(): void { Schema::create('post_tag', function (Blueprint $table) { $table->foreignId('post_id')->constrained()->cascadeOnDelete(); $table->foreignId('tag_id')->constrained()->cascadeOnDelete(); $table->primary(['post_id', 'tag_id']); $table->timestamps(); }); }
Common Pitfalls
- Dropping a column with an FK before dropping the FK — The database rejects the column drop. Drop the foreign key constraint first, then the column.
- Modifying a migration that's already been deployed — It silently diverges from production. Always create a new migration.
- Making a column non-nullable without backfilling — The ALTER fails on existing rows with
NULL. Backfill in a separate migration first.
Best Practices
- Backfill data in a separate migration before constraint changes — Safer and easier to reason about than combining them.
- Always drop indexes before their columns — The drop order matters; dropping the column first breaks the index.
- Keep each migration atomic — One logical change per file, so rollbacks are surgical.
Summary
Schema::table('users', function ($table) { ... })modifies existing tables.->change()turns a column definition into an ALTER for the existing column.- Dropping requires specific methods:
dropColumn,dropForeign,dropIndex,dropUnique. renameColumnrenames columns;Schema::renamerenames tables.- Existence checks (
hasTable,hasColumn) enable defensive migrations.