Migrations & Seeders & Command for production data [duplicate]

Recently after some research I came up with a question: how to handle default / initial data population in a Laravel project which goes live: should it go into seeders or migrations? I’ve seen two very different schools of thought.

  • Nuno Maduro says:

    Seeders should only be used for environment-specific data (local or test), and not for default system values. Database seeders become outdated over time and may no longer reflect the true state of your production data. This can introduce inconsistencies if you need to re-run migrations to deploy your app elsewhere. In contrast, migrations are inherently reliable because they define the exact structure and transformations that shaped your production data, guaranteeing consistency across environments.

    YouTube video in which Nuno talks about seeders

  • Another developer explained why they always use migrations for default values:

    • They support dependencies between tables.

    • Easier to revert a release by using down() to remove the inserted data.

    • Avoids the risk of someone forgetting to run the seeders, or accidentally running test seeders in production.

  • On the other hand, many developers say:

    Migrations should migrate schema, not seed data.

    But they often don’t address the fact that default values are actually part of the schema of the application.

In my project I have two different types of default data:

  1. Roles (user, admin): These is a critical system data, so I’m leaning toward adding them in a migration.

  2. DishCategories & DishSubcategories: Here it gets tricky. I have a large predefined array of categories and subcategories (kind of like a starter set). To avoid bloating the migration file, I moved that array into a separate PHP file and include it when inserting the data.

Is this an acceptable approach?

On the one hand, these are not “test” values, they are part of the actual application logic (used in search). On the other hand, they can evolve later by being managed in the admin panel.

Laravel Daily also mentioned a possible issue when putting data population inside migrations:

  • During automated tests, when migrations are re-run, the default data might get inserted multiple times unless you use insertOrIgnore or similar safeguards.

  • This could lead to duplicate rows in test environments.

YouTube video in which Laravel Daily talks about those 2 approaches

Also there is 3 way of seeding data using custom artisan command, but I didn’t make such feature as it is similar to seeders and I’ll need to run this command right after running migrations

A code example of these 2 approaches (Seeder / Migration)

Seeder

class RolesSeeder extends Seeder
{
    public function run(): void
    {
        Role::create(['name' => 'user']);
        Role::create(['name' => 'admin']);
    }
}

Migration

    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });

        $roles = [
            'user',
            'admin'
        ];

        foreach ($roles as $role){
            DB::table('roles')->insert([
                'name' => $role,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

There is already Laravel : Migrations & Seeding for production data on this topic, but because the question was asked a long time ago, I decided to raise this topic again