Seeders
Seeders insert rows from code: idempotent reference data for every environment, factory-made demo data for local work.
php artisan db:seed --class=RoleSeederA seeder is a class with a run() method that puts rows into the database. php artisan make:seeder RoleSeeder writes it to database/seeders.
php artisan db:seed runs exactly one class, Database\Seeders\DatabaseSeeder. Everything else is reached from there with $this->call([...]), which resolves each class from the container and invokes it. php artisan db:seed --class=RoleSeeder runs one directly, and a bare name is prefixed with Database\Seeders\ for you. php artisan migrate --seed and php artisan migrate:fresh --seed migrate and seed in one command.
Two different jobs
Reference data — roles, plans, countries, tax bands — is part of the application. It is seeded everywhere, production included, so it must be idempotent: updateOrCreate or upsert keyed on something stable, never a bare create that duplicates rows or trips a unique index on the second run. Demo data is the opposite: factory-made, local and test only, and happy to be dropped. Keep them in separate seeder classes so you can run one without the other.
Details that bite
db:seed wraps the whole run in Model::unguarded(), so mass-assignment protection is off inside a seeder and $fillable will not catch a stray key. In production the command asks for confirmation unless you pass --force. When two parent seeders both need a shared one, call it with $this->callOnce(), which skips any class already called in this process.
Example
<?php
// database/seeders/RoleSeeder.php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Role;
use Illuminate\Database\Seeder;
class RoleSeeder extends Seeder
{
/**
* Reference data. Safe to run on every deploy, in every environment.
*/
public function run(): void
{
$roles = [
['slug' => 'owner', 'name' => 'Owner', 'level' => 30],
['slug' => 'editor', 'name' => 'Editor', 'level' => 20],
['slug' => 'viewer', 'name' => 'Viewer', 'level' => 10],
];
foreach ($roles as $role) {
// First array: how to find the row. Second: what to set.
// Run it twice and the table still holds three rows.
Role::updateOrCreate(
['slug' => $role['slug']],
['name' => $role['name'], 'level' => $role['level']],
);
}
}
}When to use it
- Put the application's roles and plans into a fresh database on every environment.
- Give a new developer a usable local site with one command after migrating.
- Re-run reference data on each deploy without duplicating rows.
- Reset a test database to a known set of rows between runs.
More examples
One seeder calling the others
This is the only class db:seed knows about, so it is the index of your seeders. The WithoutModelEvents trait is read by Seeder::__invoke, which wraps the entire nested run in Model::withoutEvents(): no observers, no created or saved events, for the children as well. Use it when an observer would send mail or write an audit row for every seeded record, and leave it off when the observer is doing work the seeded rows actually need.
<?php
// database/seeders/DatabaseSeeder.php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
public function run(): void
{
// Reference data, everywhere. Order matters when one table
// has a foreign key into another.
$this->call([
RoleSeeder::class,
PlanSeeder::class,
]);
// Demo data, never in production.
if (App::environment('local', 'testing')) {
$this->call(DemoContentSeeder::class);
}
}
}Factory-made demo data
Mixing the two styles on purpose. The demo login is reference data in disguise, so it is written with updateOrCreate and survives a re-run. The bulk content is throwaway and uses the factory, which is why this class must stay out of production: it would both multiply content on every run and depend on Faker, a require-dev package. The environment check in DatabaseSeeder is what keeps it out.
<?php
// database/seeders/DemoContentSeeder.php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Post;
use App\Models\User;
use Illuminate\Database\Seeder;
class DemoContentSeeder extends Seeder
{
public function run(): void
{
// One account whose credentials everyone on the team knows.
// Re-runnable: the email is the key, the rest is overwritten.
User::updateOrCreate(
['email' => '[email protected]'],
['name' => 'Demo User', 'email_verified_at' => now()],
);
// Everything else is disposable, so a factory is right here.
User::factory(10)
->has(Post::factory()->count(3)->published())
->create();
}
}upsert for a large reference table
The fast, idempotent form for reference tables with more than a handful of rows, and the one to reach for in a seeder that runs on every deploy. The trade-off is that it skips the model layer: no events and no observers. Timestamps are the exception, because Eloquent's upsert() sets created_at and updated_at for you when the model uses them; it is DB::table()->upsert() that leaves them to you. Remember too that a missing unique index makes MySQL and MariaDB fall back to a plain insert that duplicates rows, while PostgreSQL raises an error instead.
<?php
use App\Models\Country;
// One statement instead of 250 selects and 250 writes.
// Needs a unique index on `code` for the conflict target to work.
Country::upsert(
[
['code' => 'FR', 'name' => 'France', 'vat' => 20.0],
['code' => 'DE', 'name' => 'Germany', 'vat' => 19.0],
['code' => 'IE', 'name' => 'Ireland', 'vat' => 23.0],
],
['code'], // identify an existing row by this
['name', 'vat'], // and update only these when it exists
);
// Eloquent's upsert() compiles down to a single query builder statement:
// - no model events, so observers do not fire
// - created_at and updated_at ARE filled in for you when the model uses
// timestamps, and updated_at is appended to the update columns
// - every row in the array must have the same keys
Discussion