Laravel Migrations: A Comprehensive Guide
19 May 2025 | Category: Laravel
Laravel migrations provide a powerful, database-agnostic way to define and manage your database schema using PHP code, enabling version control for database changes. Migrations allow you to create, modify, and drop tables or columns, ensuring consistent database structures across development, testing, and production environments. This SEO-friendly, plagiarism-free guide explains how to create, run, and manage migrations in Laravel, with practical examples and best practices. Based on Laravel 11 (as of May 19, 2025), this tutorial is designed for beginners and intermediate developers and complements the previous discussion on database configuration.
What are Laravel Migrations?
Migrations are PHP classes that describe database schema changes, such as creating tables, adding columns, or defining indexes. They serve as a version-control system for your database, allowing you to:
- Define schema in code.
- Apply changes incrementally.
- Roll back changes if needed.
- Share schema changes with your team via version control.
Each migration file contains two methods:
- up(): Applies the schema change (e.g., create a table).
- down(): Reverts the change (e.g., drop the table).
Migrations are stored in the database/migrations/
directory and are executed in chronological order based on their timestamped filenames.
Key Benefits
- Version Control: Track and share database changes via Git.
- Consistency: Ensure identical schemas across environments.
- Reversibility: Roll back changes with minimal effort.
- Database Agnostic: Write migrations once for MySQL, PostgreSQL, SQLite, etc.
- Team Collaboration: Simplify schema updates in team projects.
Setting Up Migrations
Before creating migrations, ensure your database is configured in the .env
file (as covered previously). For example:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret
Verify the connection:
php artisan tinker
>>> DB::connection()->getPdo();
Creating a Migration
Laravel’s Artisan CLI simplifies migration creation. Use the make:migration
command to generate a migration file.
Basic Migration
php artisan make:migration create_posts_table
This creates a file in database/migrations/
(e.g., 2025_05_19_123456_create_posts_table.php
):
<?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->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
- up(): Defines the schema (creates the
posts
table with anid
andtimestamps
). - down(): Reverts the change (drops the
posts
table). - Blueprint: A fluent interface for defining table structure.
- Schema: Facade for schema operations.
Naming Conventions
- Create Table:
create_table_name
(e.g.,create_posts_table
). - Modify Table:
add_column_to_table_name
ormodify_table_name
(e.g.,add_slug_to_posts_table
). - Descriptive Names: Use clear names to indicate the purpose (e.g.,
create_users_table
,add_status_to_orders_table
).
Defining a Migration
You can customize the up()
and down()
methods to define your schema. Below are common column types and modifiers.
Common Column Types
$table->id(); // Auto-incrementing primary key (BIGINT)
$table->string('name', 255); // VARCHAR(255)
$table->text('description'); // TEXT
$table->integer('quantity'); // INTEGER
$table->boolean('is_active')->default(true); // BOOLEAN
$table->decimal('price', 8, 2); // DECIMAL(8,2) for currency
$table->date('published_at')->nullable(); // DATE
$table->timestamp('created_at')->nullable(); // TIMESTAMP
$table->timestamps(); // created_at and updated_at
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // Foreign key
- Length: Specify for
string
(e.g.,string('name', 100)
). - Precision: For
decimal
(e.g.,8,2
for 123456.78).
Common Modifiers
- nullable(): Allows NULL values.
- default($value): Sets a default value.
- unsigned(): For non-negative integers.
- unique(): Adds a unique constraint.
- index(): Creates an index for faster queries.
- after(‘column’): Places the column after another (MySQL only).
Example: Full Migration
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 255);
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('slug')->unique()->after('title');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
- Creates a
posts
table with: - Auto-incrementing
id
. title
(VARCHAR 255).content
(TEXT).is_published
(BOOLEAN, default false).user_id
(foreign key referencingusers
, cascades on delete).slug
(unique VARCHAR aftertitle
).created_at
andupdated_at
(viatimestamps()
).
Running Migrations
Apply migrations to the database using Artisan commands. Laravel tracks applied migrations in the migrations
table.
Apply Migrations
php artisan migrate
- Runs all unapplied migrations in
database/migrations/
. - Creates the
migrations
table if it doesn’t exist.
Rollback Migrations
Revert the last batch of migrations:
php artisan migrate:rollback
- Executes the
down()
method for the most recent batch.
Other Commands
- Reset: Revert all migrations:
php artisan migrate:reset
- Refresh: Roll back and re-run all migrations:
php artisan migrate:refresh
- Fresh: Drop all tables and re-run migrations:
php artisan migrate:fresh
- Status: Check which migrations have run:
php artisan migrate:status
Seeding with Migrations
Combine migrations with seeders for initial data:
php artisan migrate:fresh --seed
Modifying Existing Tables
To modify a table (e.g., add, rename, or drop columns), create a new migration.
Example: Add a Column
php artisan make:migration add_status_to_posts_table
Migration:
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->string('status')->default('draft')->after('is_published');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('status');
});
}
- Schema::table: Modifies an existing table (vs.
Schema::create
). - after(‘is_published’): Positions the column.
Example: Rename a Column
Install doctrine/dbal
for column renaming:
composer require doctrine/dbal
Migration:
php artisan make:migration rename_status_to_state_in_posts_table
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->renameColumn('status', 'state');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->renameColumn('state', 'status');
});
}
Example: Drop a Column
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->string('slug')->unique()->after('title');
});
}
Foreign Keys and Relationships
Foreign keys enforce referential integrity. Use foreignId()
for simplicity.
Example:
$table->foreignId('user_id')->constrained()->onDelete('cascade');
- constrained(): Links to the
id
column of theusers
table. - onDelete(‘cascade’): Deletes posts if the referenced user is deleted.
- Alternatives:
onDelete('set null')
,onDelete('restrict')
.
Complex Foreign Key:
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
- More explicit syntax for custom foreign keys.
Seeding and Factories (Optional Integration)
Migrations often pair with seeders and factories to populate the database.
Create a Seeder
php artisan make:seeder PostSeeder
Seeder (database/seeders/PostSeeder.php
):
public function run(): void
{
Post::create([
'title' => 'Sample Post',
'content' => 'This is a sample post.',
'is_published' => true,
'user_id' => 1,
]);
}
Create a Factory
php artisan make:factory PostFactory
Factory (database/factories/PostFactory.php
):
public function definition(): array
{
return [
'title' => fake()->sentence,
'content' => fake()->paragraph,
'is_published' => fake()->boolean,
'user_id' => User::factory(),
];
}
Use in Seeder:
public function run(): void
{
Post::factory()->count(10)->create();
}
Run:
php artisan db:seed
Or with migrations:
php artisan migrate:fresh --seed
Best Practices for Migrations
- Use Descriptive Names:
- E.g.,
create_posts_table
,add_slug_to_posts_table
.
- Keep Migrations Atomic:
- One migration per table or change for easier rollbacks.
- Define down() Properly:
- Ensure
down()
reversesup()
accurately.
- Use Foreign Keys:
- Enforce relationships with
foreignId()->constrained()
.
- Test Migrations:
- Run migrations in a local/test environment first.
- Use
migrate:status
to verify.
- Backup Production Databases:
- Back up before running migrations in production.
- Avoid Data Modifications:
- Use seeders for data, not migrations.
- Leverage Factories:
- Use factories for consistent test data.
- Document Complex Migrations:
- Add comments or maintain schema documentation.
- Clear Cache:
- Run
php artisan config:clear
if.env
changes affect migrations.
- Run
Debugging Migrations
- Syntax Errors:
- Check migration files for typos or invalid column types.
- Connection Issues:
- Verify
.env
settings and test withDB::connection()->getPdo()
. - Migration Conflicts:
- Ensure timestamps are unique to avoid execution order issues.
- Use
migrate:status
to identify unapplied migrations. - Table Exists:
- Use
Schema::dropIfExists()
indown()
to avoid errors. - Logs:
- Check
storage/logs/laravel.log
for detailed errors. - Verbose Output:
- Run
php artisan migrate --verbose
for more details.
Example: Complete Migration Workflow
Step 1: Create a Migration
php artisan make:migration create_posts_table
Migration:
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 255);
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('slug')->unique()->after('title');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
Step 2: Create a Model and Factory
php artisan make:model Post -mf
-m
: Skips migration (already created).-f
: Creates a factory.
Model (app/Models/Post.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id', 'slug'];
}
Factory (database/factories/PostFactory.php
):
public function definition(): array
{
return [
'title' => fake()->sentence,
'content' => fake()->paragraph,
'is_published' => fake()->boolean,
'user_id' => User::factory(),
'slug' => fake()->unique()->slug,
];
}
Step 3: Run Migrations
php artisan migrate
Step 4: Seed Data
Seeder (database/seeders/PostSeeder.php
):
public function run(): void
{
Post::factory()->count(10)->create();
}
Run:
php artisan migrate:fresh --seed
Conclusion
Laravel migrations are a cornerstone of database management, offering a structured, version-controlled approach to schema changes. By creating migrations with Artisan, defining tables and columns with the schema builder, and running them with commands like migrate
and rollback
, you can maintain consistent and reversible database structures. Integrating migrations with models, factories, and seeders streamlines development and testing workflows.
Next Steps:
- Create a migration:
php artisan make:migration create_table_name
. - Define a table schema with columns and foreign keys.
- Run migrations and seed data:
php artisan migrate:fresh --seed
.
For deeper insights, explore Laravel’s official documentation or engage with the Laravel community on platforms like X. Start managing your database schema with migrations today!
Laravel Migrations Example
This artifact provides a practical example of creating and running a migration for a posts
table, including a model, factory, and seeder.
Migration (database/migrations/2025_05_19_123456_create_posts_table.php
)
<?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->string('title', 255);
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('slug')->unique()->after('title');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Model (app/Models/Post.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id', 'slug'];
}
Factory (database/factories/PostFactory.php
)
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence,
'content' => fake()->paragraph,
'is_published' => fake()->boolean,
'user_id' => User::factory(),
'slug' => fake()->unique()->slug,
];
}
}
Seeder (database/seeders/PostSeeder.php
)
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
public function run(): void
{
Post::factory()->count(10)->create();
}
}
Usage
- Ensure your
.env
file is configured (e.g., MySQL, PostgreSQL, or SQLite). - Save the migration, model, factory, and seeder files.
- Run migrations:
php artisan migrate
- Seed the database:
php artisan db:seed
Or combine:
php artisan migrate:fresh --seed
- Verify the
posts
table and data in your database.
This example demonstrates a migration for a posts
table with a foreign key to users
, a model with fillable attributes, a factory for generating fake data, and a seeder to populate the table.