Files
laravel-shopping-cart/.claude/skills/laravel-best-practices/rules/migrations.md
T
2026-04-09 16:06:44 -06:00

3.2 KiB

Migration Best Practices

Generate Migrations with Artisan

Always use php artisan make:migration for consistent naming and timestamps.

Incorrect (manually created file):

// database/migrations/posts_migration.php  ← wrong naming, no timestamp

Correct (Artisan-generated):

php artisan make:migration create_posts_table
php artisan make:migration add_slug_to_posts_table

Use constrained() for Foreign Keys

Automatic naming and referential integrity.

$table->foreignId('user_id')->constrained()->cascadeOnDelete();

// Non-standard names
$table->foreignId('author_id')->constrained('users');

Never Modify Deployed Migrations

Once a migration has run in production, treat it as immutable. Create a new migration to change the table.

Incorrect (editing a deployed migration):

// 2024_01_01_create_posts_table.php — already in production
$table->string('slug')->unique(); // ← added after deployment

Correct (new migration to alter):

// 2024_03_15_add_slug_to_posts_table.php
Schema::table('posts', function (Blueprint $table) {
    $table->string('slug')->unique()->after('title');
});

Add Indexes in the Migration

Add indexes when creating the table, not as an afterthought. Columns used in WHERE, ORDER BY, and JOIN clauses need indexes.

Incorrect:

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('status');
    $table->timestamps();
});

Correct:

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->index();
    $table->string('status')->index();
    $table->timestamp('shipped_at')->nullable()->index();
    $table->timestamps();
});

Mirror Defaults in Model $attributes

When a column has a database default, mirror it in the model so new instances have correct values before saving.

// Migration
$table->string('status')->default('pending');

// Model
protected $attributes = [
    'status' => 'pending',
];

Write Reversible down() Methods by Default

Implement down() for schema changes that can be safely reversed so migrate:rollback works in CI and failed deployments.

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

For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.

Keep Migrations Focused

One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).

Incorrect (partial failure creates unrecoverable state):

public function up(): void
{
    Schema::create('settings', function (Blueprint $table) { ... });
    DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
}

Correct (separate migrations):

// Migration 1: create_settings_table
Schema::create('settings', function (Blueprint $table) { ... });

// Migration 2: seed_default_settings
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);