creating-migrations
Create Laravel database migrations following conventions for schema changes, indexes, and foreign keys. Use when creating migrations, modifying database schema, adding tables, or when user mentions migrations, database schema, schema changes, or database structure.
When & Why to Use This Skill
This Claude skill automates the creation of Laravel database migrations, ensuring strict adherence to modern PHP conventions and database best practices. It streamlines the process of defining schemas, managing indexes, and implementing foreign key constraints, allowing developers to evolve their database structure efficiently, safely, and with full type-safety.
Use Cases
- Rapidly scaffolding new database tables with standardized primary keys and timestamp columns.
- Modifying existing database structures by adding, renaming, or updating columns while maintaining reversible migration logic.
- Implementing complex relational data models using foreign key constraints with automated cascade behaviors.
- Optimizing database query performance by generating single-column, compound, and unique indexes.
- Creating many-to-many relationship structures through the automated generation of pivot tables.
| name | creating-migrations |
|---|---|
| description | Create Laravel database migrations following conventions for schema changes, indexes, and foreign keys. Use when creating migrations, modifying database schema, adding tables, or when user mentions migrations, database schema, schema changes, or database structure. |
Create Laravel Database Migrations
Create database migrations that version schema changes with proper indexes and foreign keys. Migrations run sequentially to evolve the database structure, using timestamped filenames for ordering and descriptive names for clarity.
File Structure
Migrations are stored in database/migrations:
database/migrations/{timestamp}_{description}.php
Examples:
2024_01_01_000000_create_users_table.php2024_01_02_000000_add_avatar_to_users_table.php2024_01_03_000000_create_orders_table.php
Core Conventions
1. Migration Structure
<?php
declare(strict_types=1);
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('table_name', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('table_name');
}
};
Key Requirements:
- Always include
declare(strict_types=1); - Use anonymous class extending
Migration - Type hint closure parameter as
Blueprint - Type hint closure return as
void - Implement both
up()anddown()methods - Use
dropIfExists()indown()method
2. Column Types
$table->id(); // Auto-incrementing primary key
$table->string('email'); // VARCHAR(255)
$table->string('name', 100); // VARCHAR(100)
$table->text('description'); // TEXT
$table->integer('count'); // INTEGER
$table->boolean('is_active'); // BOOLEAN
$table->timestamp('published_at'); // TIMESTAMP
$table->timestamps(); // created_at, updated_at
$table->softDeletes(); // deleted_at
3. Indexes and Constraints
$table->string('email')->unique();
$table->index('email');
$table->index(['user_id', 'created_at']);
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
Examples
Example 1: Create Users Table
<?php
declare(strict_types=1);
use App\Support\Localisation\Facades\Localisation;
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('users', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('language')->default(Localisation::getDefaultLocale());
$table->string('password');
$table->text('two_factor_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable();
$table->rememberToken();
$table->string('avatar_path', 2048)->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->timestamp('two_factor_confirmed_at')->nullable();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table): void {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table): void {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
Example 2: Create Table with Foreign Keys
<?php
declare(strict_types=1);
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('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->string('status')->default('pending');
$table->integer('total');
$table->text('notes')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'status']);
$table->index('created_at');
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Example 3: Add Column to Existing Table
<?php
declare(strict_types=1);
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::table('users', function (Blueprint $table): void {
$table->string('phone')->nullable()->after('email');
$table->boolean('is_active')->default(true)->after('email_verified_at');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn(['phone', 'is_active']);
});
}
};
Example 4: Create Pivot Table
<?php
declare(strict_types=1);
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('order_product', function (Blueprint $table): void {
$table->foreignId('order_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('product_id')
->constrained()
->cascadeOnDelete();
$table->integer('quantity')->default(1);
$table->integer('price');
$table->timestamps();
$table->primary(['order_id', 'product_id']);
});
}
public function down(): void
{
Schema::dropIfExists('order_product');
}
};
Example 5: Modify Column
<?php
declare(strict_types=1);
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::table('users', function (Blueprint $table): void {
$table->string('email', 320)->change();
$table->text('bio')->nullable()->change();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('email', 255)->change();
$table->string('bio', 500)->nullable()->change();
});
}
};
Foreign Key Conventions
// Basic foreign key
$table->foreignId('user_id')->constrained();
// With cascade delete
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
// With cascade update
$table->foreignId('user_id')
->constrained()
->cascadeOnUpdate();
// With set null
$table->foreignId('user_id')
->nullable()
->constrained()
->nullOnDelete();
// Reference specific table
$table->foreignId('owner_id')
->constrained('users')
->cascadeOnDelete();
Indexes
// Single column index
$table->index('email');
// Compound index
$table->index(['user_id', 'created_at']);
// Unique index
$table->unique('email');
// Named index
$table->index('email', 'users_email_index');
Anti-Patterns
❌ Don't Do This
// Don't skip type hints on closure
Schema::create('users', function ($table) { // ❌ Type hint Blueprint
// Don't skip return type on closure
Schema::create('users', function (Blueprint $table) { // ❌ Add : void
// Don't use drop() in down()
public function down(): void
{
Schema::drop('users'); // ❌ Use dropIfExists()
}
// Don't forget indexes
$table->foreignId('user_id')->constrained(); // ❌ Add cascadeOnDelete()
// Don't use class keyword for migration
final class CreateUsersTable extends Migration // ❌ Use anonymous class
{
}
// Don't forget timestamps
$table->id();
$table->string('name');
// ❌ Missing $table->timestamps()
✅ Do This Instead
// Type hint closure parameter
Schema::create('users', function (Blueprint $table): void {
// Add return type on closure
Schema::create('users', function (Blueprint $table): void {
// Use dropIfExists()
public function down(): void
{
Schema::dropIfExists('users');
}
// Add appropriate cascades
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
// Use anonymous class
return new class extends Migration
{
}
// Always add timestamps
$table->id();
$table->string('name');
$table->timestamps();
Running Migrations
# Run migrations
php artisan migrate
# Rollback last batch
php artisan migrate:rollback
# Rollback all migrations
php artisan migrate:reset
# Fresh database (drop all tables)
php artisan migrate:fresh
# Fresh with seeders
php artisan migrate:fresh --seed
Quality Standards
- All migrations must pass PHPStan level 8
- Code formatted with Pint
- Proper foreign key constraints
- Appropriate indexes for query performance
- Reversible migrations (proper
down()method) - Named consistently with timestamp prefix