Laravel Migrations¶
Migrations are version control for the database schema. Each migration is a PHP class that creates, modifies, or drops tables and columns. Migrations run in order and can be rolled back. They work with [[laravel-eloquent-orm]] models and support foreign key constraints for relationships.
Key Facts¶
- Migrations live in
database/migrations/with timestamped filenames php artisan make:migration create_posts_tablegenerates a migration classphp artisan make:model Post -mcreates model + migration togetherup()method applies changes;down()method reverses themphp artisan migrateruns all pending migrationsphp artisan migrate:rollbackundoes the last batchphp artisan migrate:freshdrops all tables and re-runs all migrations (destructive)- Foreign keys enforce referential integrity between tables
$table->softDeletes()addsdeleted_atcolumn for [[laravel-eloquent-orm]] soft deletes- Default migrations create
users,password_reset_tokens,sessions,cache,jobstables - SQLite is the default database in Laravel 11; switch to MySQL/MariaDB via
.env
Patterns¶
Creating a table¶
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id(); // bigint unsigned auto-increment PK
$table->string('title'); // varchar(255)
$table->string('slug')->unique(); // unique index
$table->text('body'); // text
$table->string('image')->nullable(); // nullable varchar
$table->boolean('is_published')->default(false);
$table->foreignId('category_id') // bigint unsigned
->constrained() // foreign key to categories.id
->onDelete('cascade'); // delete posts when category deleted
$table->foreignId('user_id')
->constrained();
$table->softDeletes(); // deleted_at timestamp
$table->timestamps(); // created_at, updated_at
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Many-to-many pivot table¶
<?php
// Pivot table for Post <-> Tag (many-to-many)
// Naming convention: alphabetical order, singular - post_tag
return new class extends Migration
{
public function up(): void
{
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
// Prevent duplicate assignments
$table->unique(['post_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('post_tag');
}
};
Modifying existing tables¶
<?php
// Add column to existing table
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->tinyInteger('role')->default(0)->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};
Column types reference¶
<?php
Schema::create('example', function (Blueprint $table) {
// Numeric
$table->id(); // BIGINT UNSIGNED AUTO_INCREMENT PK
$table->bigInteger('count'); // BIGINT
$table->integer('quantity'); // INT
$table->tinyInteger('role'); // TINYINT (0-255)
$table->float('price', 8, 2); // FLOAT
$table->decimal('amount', 10, 2); // DECIMAL
// String
$table->string('name', 100); // VARCHAR(100)
$table->text('body'); // TEXT
$table->longText('content'); // LONGTEXT
// Date/Time
$table->timestamp('published_at'); // TIMESTAMP
$table->date('birth_date'); // DATE
$table->timestamps(); // created_at + updated_at
// Boolean
$table->boolean('is_active'); // TINYINT(1)
// JSON
$table->json('meta'); // JSON
// Modifiers
$table->string('avatar')->nullable(); // allows NULL
$table->integer('sort')->default(0); // default value
$table->string('email')->unique(); // unique index
$table->integer('position')->unsigned(); // unsigned
$table->softDeletes(); // deleted_at nullable timestamp
$table->string('bio')->after('name'); // column position (MySQL)
});
Artisan migration commands¶
# Generate migration
php artisan make:migration create_posts_table
php artisan make:migration add_role_to_users_table
# Run migrations
php artisan migrate # execute pending
php artisan migrate --seed # migrate + seed
# Rollback
php artisan migrate:rollback # undo last batch
php artisan migrate:rollback --step=2 # undo last 2 batches
php artisan migrate:reset # undo all migrations
# Fresh start (drops all tables)
php artisan migrate:fresh # drop all + re-migrate
php artisan migrate:fresh --seed # drop all + re-migrate + seed
# Status
php artisan migrate:status # show migration status
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
| Foreign key constraint error | Referenced table doesn't exist yet | Ensure referenced table migration runs first (earlier timestamp) |
Cannot delete parent row | Child records exist with foreign key | Use onDelete('cascade') or delete children first |
Column already exists | Migration ran partially or was modified after running | Run migrate:fresh in development; create new migration in production |
| Default SQLite can't handle some features | SQLite lacks ALTER TABLE DROP COLUMN (older versions) | Switch to MySQL in .env for full feature support |
| Adding column to existing table fails | Using Schema::create instead of Schema::table | Use Schema::table() for modifications |
| Rollback doesn't undo changes | down() method not implemented | Always implement down() that reverses up() |
See Also¶
- [[laravel-eloquent-orm]] - models that map to migrated tables
- [[laravel-architecture]] - database configuration in
.env - https://laravel.com/docs/11.x/migrations
- https://laravel.com/docs/11.x/seeding