.cursor/skills/laravel-best-practices/rules/migrations.md
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
constrained() for Foreign KeysAutomatic naming and referential integrity.
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
// Non-standard names
$table->foreignId('author_id')->constrained('users');
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 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();
});
$attributesWhen 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',
];
down() Methods by DefaultImplement 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.
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']);