In one sentence

A migration is a PHP file that describes one change to your database structure, so every copy of your app can build the exact same tables.

๐Ÿ—๏ธThink of migrations as numbered building instructions for a LEGO set. Anyone who follows steps 1, 2, 3 in order ends up with the same model, and if step 3 was a mistake, you can undo just that step.

Why it matters

Your schema deserves version control too

๐Ÿค

Teammates stay in sync

Pull the latest code, run php artisan migrate, and your local database matches everyone else's. No more emailing SQL dumps.

๐Ÿ•ฐ๏ธ

Changes have history

Migrations live in Git next to your code, so you can see when a column was added, by whom, and why.

๐Ÿš€

Deploys are repeatable

The same files that built your laptop database build the production one. Laravel remembers which ones already ran.

The big picture

From PHP file to real table

โŒจ๏ธmake:migrationcreates a timestamped file
๐Ÿ“You edit up()describe the table in PHP
โ–ถ๏ธmigrateruns every new file in order
๐Ÿ—„๏ธDatabasetable exists + logged in migrations
Laravel records each finished migration in a special migrations table, so it never runs the same file twice.

Migrations live in database/migrations/. The filename starts with a timestamp, which is how Laravel knows the order to run them in.

database/migrations/
โ”œโ”€โ”€ 0001_01_01_000000_create_users_table.php      โ† ships with Laravel
โ”œโ”€โ”€ 0001_01_01_000001_create_cache_table.php
โ”œโ”€โ”€ 0001_01_01_000002_create_jobs_table.php
โ”œโ”€โ”€ 2025_03_10_120000_create_categories_table.php โ† yours
โ””โ”€โ”€ 2025_03_10_120500_create_posts_table.php      โ† runs after categories
Step by step

Build the blog's tables

We'll create the categories and posts tables for our running blog example.

  1. Generate a migration

    Artisan creates the file for you. If the name follows the pattern create_xxx_table, Laravel even pre-fills the table name.

    
              php artisan make:migration create_categories_table
              php artisan make:migration create_posts_table
            
    โ„น๏ธ Shortcut

    php artisan make:model Post -m creates the model and its migration in one go. You'll meet models in the next lesson.

  2. Understand up() and down()

    Every migration has two methods. up() makes the change, and down() undoes it exactly. The Schema facade and a Blueprint object ($table) let you describe columns in PHP instead of SQL.

    
              <?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('categories', function (Blueprint $table) {
                          $table->id();                       // BIGINT auto-increment primary key
                          $table->string('name');             // VARCHAR(255)
                          $table->string('slug')->unique();   // no two categories share a slug
                          $table->timestamps();               // created_at + updated_at
                      });
                  }
    
                  public function down(): void
                  {
                      Schema::dropIfExists('categories');
                  }
              };
            
  3. Pick the right column types

    Each $table->... call adds one column. Chain modifiers such as ->nullable() or ->default() to fine-tune it.

    MethodStoresBlog example
    id()Auto-increment big integer primary keyid
    string('x', 255)Short text (VARCHAR)title, slug
    text('x')Long textbody
    boolean('x')true / falseis_featured
    unsignedInteger('x')Whole numbers โ‰ฅ 0views
    timestamp('x')Date + timepublished_at
    json('x')Structured datameta
    foreignId('x')A link to another table's iduser_id
  4. Link tables with foreign keys

    A foreign key is a column that points at a row in another table. foreignId('user_id')->constrained() creates the column and tells the database it must match a real users.id. Laravel guesses the table from the column name.

    
              public function up(): void
              {
                  Schema::create('posts', function (Blueprint $table) {
                      $table->id();
                      $table->foreignId('user_id')->constrained()->cascadeOnDelete();
                      $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
                      $table->string('title');
                      $table->string('slug')->unique();
                      $table->text('body');
                      $table->unsignedInteger('views')->default(0);
                      $table->timestamp('published_at')->nullable();
                      $table->timestamps();
                  });
              }
    
              public function down(): void
              {
                  Schema::dropIfExists('posts');
              }
            

    cascadeOnDelete() means "delete a user, and their posts go too". nullOnDelete() means "delete a category, and its posts simply become uncategorised". Because posts points at categories, the categories migration must run first, which is why its timestamp is earlier.

  5. Run, roll back, start fresh

    These commands are your daily tools. Rollback runs down() on the most recent batch of migrations.

    
              php artisan migrate              # run every migration that hasn't run yet
              php artisan migrate:status       # see which ones have run
              php artisan migrate:rollback     # undo the last batch
              php artisan migrate:fresh        # DROP ALL tables, then migrate from scratch
              php artisan migrate:fresh --seed # same, then fill with seed data
            
    โš ๏ธ migrate:fresh deletes everything

    It's perfect on your laptop and catastrophic in production. Never run it against a database holding real users' data.

  6. Change an existing table with a new migration

    Once a migration has run anywhere other than your own machine, treat it as frozen. To add an excerpt column later, write a new migration that alters the table.

    
              php artisan make:migration add_excerpt_to_posts_table
            
    
              public function up(): void
              {
                  Schema::table('posts', function (Blueprint $table) {
                      $table->string('excerpt', 300)->nullable()->after('title');
                  });
              }
    
              public function down(): void
              {
                  Schema::table('posts', function (Blueprint $table) {
                      $table->dropColumn('excerpt');
                  });
              }
            

    Note Schema::table() (change an existing table) versus Schema::create() (make a new one).

Common mistakes

Avoid this, do that

โŒ Avoid

          // Editing create_posts_table.php
          // after it already ran on staging
          $table->string('excerpt');
        

Laravel thinks that file already ran, so the column never appears on any server that migrated before.

โœ… Do

          php artisan make:migration add_excerpt_to_posts_table
          php artisan migrate
        

A new migration is picked up everywhere, and its down() lets you undo it cleanly.

โŒ Avoid

          public function down(): void
          {
              //
          }
        

An empty down() means rollback silently does nothing, leaving your schema half-changed.

โœ… Do

          public function down(): void
          {
              Schema::dropIfExists('posts');
          }
        

Make down() the exact mirror of up() so rollbacks are always safe.

๐Ÿ’ก Pro tip: the "edit freely" window

While a migration exists only on your machine and hasn't been pushed, it's fine to edit it and run php artisan migrate:fresh. The rule "never edit a migration" kicks in the moment someone else could have run it.

๐Ÿ‹๏ธ Try it yourself

Extend the blog schema:

  1. Create a comments table with id, post_id (cascade on delete), user_id, a body text column and timestamps.
  2. Create a tags table with a unique name.
  3. Run php artisan migrate, then php artisan migrate:status to confirm they ran.
  4. Roll back once with migrate:rollback and check which tables disappeared.
  5. Add a is_featured boolean (default false) to posts using a new migration.
Recap

Key takeaways

Check yourself

Mini quiz