Migrations and schema

Laravel · Essentials · 12 min

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 migrate runs pending migrations; the migrations table records which ran.
  • migrate:rollback undoes the last batch; migrate:status lists state.
  • migrate:fresh --seed drops 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 ... DONE

Remember

  • 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.

Check yourself

  1. A teammate's migration already ran in production but has a typo in a column name. What should you do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/migrations