A migration defines the shape of a table; a seeder puts rows into it. Laravel treats seeding as a distinct concern from schema changes for a good reason — the data a seeder inserts is either data every environment genuinely needs (a default admin role, a list of countries) or throwaway fake data for local development, and those two cases call for different approaches.
1. Creating a seeder
php artisan make:seeder RoleSeeder
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class RoleSeeder extends Seeder
{
public function run(): void
{
DB::table('roles')->insert([
['name' => 'admin'],
['name' => 'editor'],
['name' => 'viewer'],
]);
}
}
2. Registering it in DatabaseSeeder
// database/seeders/DatabaseSeeder.php
public function run(): void
{
$this->call([
RoleSeeder::class,
]);
}
php artisan db:seed without arguments only runs DatabaseSeeder — any seeder not listed in its call() array never runs unless invoked individually. This registration step is easy to forget after generating a new seeder class.
3. Running seeders
php artisan db:seed # runs DatabaseSeeder (and whatever it calls)
php artisan db:seed --class=RoleSeeder # runs one specific seeder directly
php artisan migrate:fresh --seed # drop everything, re-migrate, then seed
4. Seeding real lookup data vs. fake development data
These are genuinely different use cases and mixing them in one seeder gets confusing fast:
- Lookup/reference data (roles, permissions, a fixed list of categories) — data every environment, including production, actually needs. Write this with plain
DB::table()->insert()orModel::create()calls, with real, specific values. - Fake development data (500 sample orders to test pagination, realistic-looking test users) — never something you want in production. Use a model factory together with the seeder instead of hardcoding fake values by hand.
// UserSeeder — development-only fake data
public function run(): void
{
\App\Models\User::factory()->count(50)->create();
}
5. Keeping seeders idempotent
Running db:seed a second time with a plain insert() call duplicates every row — for lookup data that should exist exactly once, use updateOrCreate() or firstOrCreate() instead, so re-running the seeder is safe:
foreach (['admin', 'editor', 'viewer'] as $name) {
\App\Models\Role::firstOrCreate(['name' => $name]);
}
6. Never run migrate:fresh --seed against production
migrate:fresh drops every table before rebuilding them — it's meant for local development and CI, never a database with real user data. Seeding a production database (adding a new fixed lookup row, for instance) should go through a normal migration or a dedicated, carefully-reviewed one-off script instead.