Migrations version-control your schema so every developer and server has the same database structure.
A migration is a PHP class in database/migrations that changes the schema. Generate one with php artisan make:migration create_posts_table (or make:model Post -m). It returns an anonymous class with up() to apply the change and down() to reverse it.
The schema builder describes tables in PHP, and Laravel emits the right SQL for SQLite, MySQL, MariaDB, PostgreSQL or SQL Server:
Schema::create('posts', function (Blueprint $table) {
$table->id(); // big auto-increment primary key
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body')->nullable();
$table->timestamps(); // created_at, updated_at
});Common column types: string, text, integer, boolean, decimal('price', 8, 2), json, date, timestamp, uuid. Modifiers: nullable(), default(...), unique(), index(). Change an existing table with Schema::table('posts', ...).
Commands:
php artisan migrateruns pending migrations; themigrationstable records which ran.migrate:rollbackundoes the last batch;migrate:statuslists state.migrate:fresh --seeddrops everything and rebuilds, for local development only.
Never edit a migration that has already run in production; write a new one. Seeders (db:seed) and model factories fill the database with sample data.
Example
<?php
// database/migrations/2025_06_01_000000_create_posts_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();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamp('published_at')->nullable()->index();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
// $ php artisan migrate -> 2025_06_01_000000_create_posts_table ... DONERemember
- Each migration has up() and down() and runs once per database
- foreignId()->constrained() creates the column and the foreign key
- Write new migrations instead of editing ones that already ran
Watch out for
- Running migrate:fresh against a shared or production database, which drops all tables.