Laravel Migrations: How to Create and Modify Your Database Tables

Published on November 10, 2025
Laravel Migrations Database Schema PHP Eloquent

What are Laravel Migrations?

Migrations are like version control for your database. They allow you to define and share your database schema in code, making it easy to create, modify, and rollback database tables across different environments.

Think of migrations as:

  • Blueprints for your database tables
  • Tracked changes to your database structure
  • Team-friendly database schema management
  • Deployment-safe database modifications

Creating Your First Migration

Using Artisan Commands

Create a New Migration

# Basic migration
php artisan make:migration create_users_table

# Create migration with model
php artisan make:model User -m

# Create specific table migration
php artisan make:migration create_posts_table

# Create migration for modifying existing table
php artisan make:migration add_category_id_to_posts_table

# Create pivot table migration
php artisan make:migration create_post_tag_table

Migration File Structure

When you run php artisan make:migration create_users_table, Laravel creates a file like:

<?php
// database/migrations/2024_01_15_000000_create_users_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id(); // Auto-incrementing BIGINT primary key
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps(); // created_at and updated_at
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Migration Methods Explained

The up() Method

  • Purpose: Apply the migration (create tables, add columns, etc.)
  • When called: When running php artisan migrate
  • Contains: Database changes you want to make

The down() Method

  • Purpose: Reverse the migration (drop tables, remove columns)
  • When called: When running php artisan migrate:rollback
  • Contains: Code to undo what up() did

Creating Tables with Schema Builder

Basic Table Creation

public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->foreignId('user_id')->constrained();
        $table->timestamps();
    });
}

Complete Blog Schema Example

public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title', 200);
        $table->string('slug')->unique();
        $table->text('excerpt')->nullable();
        $table->longText('content');
        $table->string('featured_image')->nullable();
        $table->boolean('is_published')->default(false);
        $table->timestamp('published_at')->nullable();
        $table->integer('views_count')->default(0);
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->foreignId('category_id')->constrained();
        $table->timestamps();
        $table->softDeletes(); // Adds deleted_at column
        
        // Indexes for better performance
        $table->index(['is_published', 'published_at']);
        $table->index('slug');
        $table->index('user_id');
    });
}

Column Types Available in Laravel

Common Column Types

Schema::create('examples', function (Blueprint $table) {
    // Primary Key
    $table->id(); // BIGINT auto-increment
    $table->uuid('uuid')->primary(); // UUID primary key
    
    // Strings and Text
    $table->string('name', 100); // VARCHAR with length
    $table->text('description'); // TEXT
    $table->longText('content'); // LONGTEXT
    $table->char('code', 10); // CHAR with fixed length
    
    // Numbers
    $table->integer('votes'); // INTEGER
    $table->tinyInteger('status'); // TINYINT
    $table->unsignedInteger('user_count'); // UNSIGNED INTEGER
    $table->bigInteger('views'); // BIGINT
    $table->decimal('price', 8, 2); // DECIMAL(8,2)
    $table->double('rating', 3, 1); // DOUBLE(3,1)
    $table->float('amount'); // FLOAT
    
    // Date and Time
    $table->date('birth_date'); // DATE
    $table->dateTime('published_at'); // DATETIME
    $table->time('start_time'); // TIME
    $table->timestamp('email_verified_at'); // TIMESTAMP
    $table->timestamps(); // created_at & updated_at
    $table->softDeletes(); // deleted_at
    
    // Boolean
    $table->boolean('is_active'); // BOOLEAN/TINYINT(1)
    
    // JSON
    $table->json('meta'); // JSON column
    $table->jsonb('settings'); // JSONB (PostgreSQL)
    
    // Special Types
    $table->enum('difficulty', ['easy', 'medium', 'hard']);
    $table->ipAddress('visitor_ip');
    $table->macAddress('device_mac');
    $table->uuid('public_id');
    $table->year('release_year');
});

Column Modifiers

Adding Constraints and Modifiers

Schema::create('products', function (Blueprint $table) {
    $table->id();
    
    // Nullable
    $table->string('description')->nullable();
    
    // Default values
    $table->integer('stock')->default(0);
    $table->boolean('in_stock')->default(true);
    $table->string('currency')->default('USD');
    
    // Unique constraints
    $table->string('sku')->unique();
    $table->string('slug')->unique();
    
    // Indexes
    $table->string('name')->index();
    $table->decimal('price')->index();
    
    // Multiple column unique
    $table->unique(['store_id', 'product_code']);
    
    // Fulltext index (MySQL)
    $table->text('content')->fulltext();
    
    // Spatial index (PostgreSQL/MySQL)
    $table->point('location')->spatialIndex();
    
    // Comment
    $table->string('status')->comment('Product status: active, inactive, draft');
});

Foreign Keys and Relationships

Defining Foreign Key Constraints

public function up(): void
{
    Schema::create('comments', function (Blueprint $table) {
        $table->id();
        $table->text('body');
        
        // Simple foreign key
        $table->foreignId('user_id')->constrained();
        
        // With custom table name
        $table->foreignId('author_id')->constrained('users');
        
        // With on delete cascade
        $table->foreignId('post_id')
              ->constrained()
              ->onDelete('cascade');
              
        // With on delete set null
        $table->foreignId('parent_id')
              ->nullable()
              ->constrained('comments')
              ->onDelete('set null');
        
        $table->timestamps();
    });
}

Manual Foreign Key Definition

$table->unsignedBigInteger('category_id');
$table->foreign('category_id')
      ->references('id')
      ->on('categories')
      ->onDelete('cascade');

Modifying Existing Tables

Adding Columns to Existing Table

php artisan make:migration add_featured_image_to_posts_table
public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->string('featured_image')->after('title')->nullable();
        $table->integer('reading_time')->after('content')->default(1);
        $table->boolean('is_featured')->after('is_published')->default(false);
    });
}

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropColumn(['featured_image', 'reading_time', 'is_featured']);
    });
}

Modifying Columns

public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        // Change column type
        $table->string('name', 100)->change();
        
        // Make column nullable
        $table->string('phone')->nullable()->change();
        
        // Add default value
        $table->string('timezone')->default('UTC')->change();
    });
}

// Note: Requires doctrine/dbal package for column modifications
// composer require doctrine/dbal

Renaming Columns

public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->renameColumn('is_published', 'published');
    });
}

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->renameColumn('published', 'is_published');
    });
}

Dropping Columns

public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropColumn('old_column');
        $table->dropColumn(['column1', 'column2', 'column3']);
    });
}

Creating Indexes

Adding Indexes to Tables

public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        // Single column index
        $table->index('slug');
        
        // Unique index
        $table->unique('email');
        
        // Composite index
        $table->index(['user_id', 'created_at']);
        
        // Full-text index
        $table->fullText('content');
        
        // Spatial index
        $table->spatialIndex('location');
    });
}

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropIndex(['slug']);
        $table->dropUnique(['email']);
        $table->dropIndex(['user_id', 'created_at']);
        $table->dropFullText(['content']);
        $table->dropSpatialIndex(['location']);
    });
}

Pivot Tables for Many-to-Many Relationships

Creating Pivot Tables

php artisan make:migration create_post_tag_table
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->integer('order')->default(0);
        $table->timestamps();
        
        // Prevent duplicate relationships
        $table->unique(['post_id', 'tag_id']);
    });
}

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

Running Migrations

Basic Migration Commands

# Run all pending migrations
php artisan migrate

# Show migration status
php artisan migrate:status

# Rollback last migration batch
php artisan migrate:rollback

# Rollback specific number of migrations
php artisan migrate:rollback --step=3

# Rollback all migrations
php artisan migrate:reset

# Rollback and re-run all migrations
php artisan migrate:refresh

# Refresh and seed the database
php artisan migrate:refresh --seed

# Run migrations for production
php artisan migrate --force

Migration Groups

# Run specific migration group
php artisan migrate --path=/database/migrations/2024_01_15_000000_create_users_table.php

# Run migrations from specific folder
php artisan migrate --path=/database/migrations/custom/

Real-World Migration Examples

Complete E-commerce Schema

<?php
// database/migrations/2024_01_15_000001_create_ecommerce_tables.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
    {
        // Categories table
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->string('image')->nullable();
            $table->boolean('is_active')->default(true);
            $table->integer('order')->default(0);
            $table->foreignId('parent_id')->nullable()->constrained('categories');
            $table->timestamps();
            $table->softDeletes();
            
            $table->index(['is_active', 'order']);
        });

        // Products table
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->text('short_description')->nullable();
            $table->decimal('price', 10, 2);
            $table->decimal('sale_price', 10, 2)->nullable();
            $table->string('sku')->unique();
            $table->integer('stock_quantity')->default(0);
            $table->boolean('in_stock')->default(true);
            $table->boolean('is_featured')->default(false);
            $table->boolean('is_active')->default(true);
            $table->json('images')->nullable();
            $table->json('specifications')->nullable();
            $table->foreignId('category_id')->constrained();
            $table->foreignId('brand_id')->constrained();
            $table->timestamps();
            $table->softDeletes();
            
            $table->index(['is_active', 'is_featured']);
            $table->index(['category_id', 'is_active']);
            $table->fullText(['name', 'description']);
        });

        // Orders table
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('order_number')->unique();
            $table->foreignId('user_id')->constrained();
            $table->decimal('subtotal', 10, 2);
            $table->decimal('tax', 10, 2)->default(0);
            $table->decimal('shipping', 10, 2)->default(0);
            $table->decimal('total', 10, 2);
            $table->string('status')->default('pending'); // pending, processing, completed, cancelled
            $table->text('notes')->nullable();
            $table->json('billing_address');
            $table->json('shipping_address');
            $table->timestamp('paid_at')->nullable();
            $table->timestamp('shipped_at')->nullable();
            $table->timestamps();
            
            $table->index(['user_id', 'created_at']);
            $table->index('status');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('order_items');
        Schema::dropIfExists('orders');
        Schema::dropIfExists('products');
        Schema::dropIfExists('categories');
        Schema::dropIfExists('brands');
    }
};

Migration Best Practices

1. Use Descriptive Migration Names

# ✅ GOOD
php artisan make:migration create_posts_table
php artisan make:migration add_published_at_to_posts_table
php artisan make:migration create_post_tag_pivot_table

# ❌ BAD
php artisan make:migration update_table
php artisan make:migration add_columns

2. Keep Migrations Small and Focused

// ✅ GOOD: One migration per logical change
public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->boolean('is_featured')->default(false);
    });
}

// ❌ BAD: Multiple unrelated changes
public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->boolean('is_featured')->default(false);
    });
    
    Schema::table('users', function (Blueprint $table) {
        $table->string('phone')->nullable();
    });
}

3. Always Define the down() Method

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropColumn('is_featured');
    });
}

4. Use Foreign Key Constraints

// ✅ GOOD
$table->foreignId('user_id')->constrained()->onDelete('cascade');

// ❌ BAD
$table->integer('user_id');

5. Add Indexes for Performance

// Add indexes for frequently queried columns
$table->index('email');
$table->index(['status', 'created_at']);
$table->unique('slug');

Common Migration Issues and Solutions

1. Migration Already Exists Error

# Reset and re-run
php artisan migrate:reset
php artisan migrate

# Or manually delete the migration from migrations table

2. Foreign Key Constraint Fails

// Ensure tables are created in correct order
// Users table should exist before posts table

// Or use separate migrations
public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->unsignedBigInteger('user_id'); // Add without constraint first
        $table->timestamps();
    });
    
    // Add foreign key in separate migration
    Schema::table('posts', function (Blueprint $table) {
        $table->foreign('user_id')->references('id')->on('users');
    });
}

3. Column Modification Requires Doctrine DBAL

composer require doctrine/dbal

Common Interview Questions & Answers

1. What are Laravel migrations?

Migrations are version control for your database schema. They allow you to define database changes in code and apply them consistently across different environments.

2. How do you create a migration?

Use php artisan make:migration migration_name. The migration file will be created in the database/migrations directory.

3. What's the difference between up() and down() methods?

The up() method applies the migration changes, while down() reverses them. This allows for rollbacks and ensures database changes are reversible.

4. How do you add a foreign key constraint?

Use $table->foreignId('user_id')->constrained() or manually with $table->foreign('user_id')->references('id')->on('users').

5. What's the purpose of timestamps() and softDeletes()?

timestamps() adds created_at and updated_at columns, while softDeletes() adds a deleted_at column for soft deletion functionality.

6. How do you rollback a migration?

Use php artisan migrate:rollback to rollback the last batch, or php artisan migrate:rollback --step=3 to rollback specific number of migrations.

Migration Command Cheat Sheet

# Create migrations
php artisan make:migration create_table_name
php artisan make:migration add_column_to_table
php artisan make:model ModelName -m

# Run migrations
php artisan migrate
php artisan migrate:status
php artisan migrate --force

# Rollback migrations
php artisan migrate:rollback
php artisan migrate:rollback --step=3
php artisan migrate:reset
php artisan migrate:refresh
php artisan migrate:refresh --seed

Now you're equipped to manage your database schema like a pro with Laravel migrations! In our next post, we'll explore Laravel Eloquent ORM: What is it and Why is it a Game-Changer? to learn how to interact with your database using elegant, expressive syntax.