Laravel test data: one FK-consistent seeder instead of a factory per table
Writing a Laravel factory and seeder per table by hand rots the moment the schema moves, and copying a production dump into your dev database is a GDPR problem waiting to happen. The fix: foreign-key-consistent, realistic data generated straight from your schema, data that actually looks real, not "Premium Widget 1" and lorem ipsum. One seeder fills the whole database, deterministic per seed.
Free tier, no card. Want to see the output before wiring anything up? Try the login-free sandbox: paste a schema, generate, look at the rows.
On a different stack? Symfony test data · Django test data · generate from raw SQL
Seed your database with one command
Pull SeedBase in as a dev dependency and run the supplied seeder. It generates and loads foreign-key-consistent data over the Laravel database connection, so your schema has to exist first, which your migrations already own.
php artisan db:seed --class="Seedbase\Laravel\SeedbaseSeeder"
Install it with Composer:
composer require --dev seedbase/seedbase
The supplied SeedbaseSeeder does the work: it triggers one generation, waits for it, downloads the SQL dump, and imports it inside a single transaction over the Laravel connection your app uses. No per-table seeder, no manual wiring between parents and children.
Laravel seeder: what the bundled SeedbaseSeeder runs
The bundled seeder is thin on purpose. It reads your project id from config('seedbase.project'), runs one deterministic generation, pulls the SQL, and loads it transactionally so a failed import rolls back cleanly instead of leaving half a database. That is the whole class:
<?php
namespace App\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Seedbase\Laravel\SeedbaseClient;
class DatabaseSeeder extends Seeder
{
public function __construct(private SeedbaseClient $seedbase) {}
public function run(): void
{
$gen = $this->seedbase->generate(config('seedbase.project'), [
'seed' => 42,
'wait' => true,
]);
$sql = $this->seedbase->download($gen['id'], 'sql');
DB::transaction(function () use ($sql) {
DB::unprepared($sql);
});
}
}
The SeedbaseClient is resolved out of the container, so you can inject it into any seeder, command or test and drive generation yourself instead of going through the prebuilt Seedbase\Laravel\SeedbaseSeeder. 'wait' => true blocks until the generation finishes; download($gen['id'], 'sql') hands you the dump.
Laravel factory data the engine resolves, not you
A model factory describes one table. The relationships between tables, which parent rows exist, which foreign keys to satisfy, which order to insert in, are work you do by hand with ->for(), ->has() and explicit ordering. SeedBase reads the schema and resolves all of that, so you ask for a dataset, not for each row's parents:
<?php
use Seedbase\Laravel\SeedbaseClient;
it('reports revenue across customers and orders', function () {
$client = app(SeedbaseClient::class);
$gen = $client->generate(config('seedbase.project'), [
'seed' => 7,
'wait' => true,
]);
DB::unprepared($client->download($gen['id'], 'sql'));
expect(Order::whereDoesntHave('customer')->count())->toBe(0);
});
Every order.customer_id points at a customer that exists, every nullable column is sometimes null, and totals reconcile with their line items, because the engine generates across tables rather than one factory at a time. It complements Eloquent factories and seeders rather than replacing the idea: keep a factory where you want a single hand-built row, reach for a generated dataset when you need a whole schema filled FK-consistent.
Realistic data, not "Premium Widget 1"
The point is not just filling rows, it is filling them with data a real query would actually return. SeedBase resolves foreign keys, uses realistic distributions, and writes coherent free text instead of repeating a placeholder, so a customer_id on an order points at a customer that exists and a product name reads like a product, not "Premium Widget 1".
- Foreign keys resolve: every child row points at a parent that exists, so the data loads with constraints on.
- Realistic distributions and coherent free text, not placeholders, so your queries and assertions hit data that looks real.
- One seeder replaces the pile of per-table factories and the wiring between them.
A real alternative to hand-written factories and seeders
A handful of factories is fine. A real schema is not. Once you have dozens of tables, every factory has to know which parent rows exist, which foreign keys to satisfy, and which derived totals to keep in sync. You end up maintaining a second, worse copy of your schema in test code.
- Factories drift: add a NOT NULL column and every factory that misses it starts failing.
- Seeders multiply: one seeder per table, plus all the ordering so children come after their parents.
- Production dumps are a GDPR problem: real names, real emails, real card data sitting in your dev database.
- The data looks fake, so tests pass against rows no real query would ever return.
SeedBase replaces all of that with one seeder that reads your schema, resolves the foreign keys for you, and seeds realistic data in the right order.
Deterministic and reproducible
Generation is seeded. The same seed produces the same data, every run. That keeps CI reproducible: the same seeder run yields the same rows on every machine, so a green build stays green for the right reason. When a test fails, reuse the seed and you reproduce the exact dataset that broke it, instead of chasing a seeder that has since been hand-edited.
Free tier: generate, download, seed
Generation and JSON download are free to use. You do not need push-to-database or any paid feature to seed your Laravel database, because the data loads through your own database connection. Generate once, run the seeder, and let it fill the schema your migrations already own.
Try it
Pull the package in, drop the seeder in, point it at your schema, and watch a real FK-consistent dataset get written to your database.
composer require --dev seedbase/seedbase
php artisan db:seed --class="Seedbase\Laravel\SeedbaseSeeder"
- Paste a schema and look at the rows in the login-free sandbox, no account needed.
- On a different stack? See the Symfony test data page, or generate from raw SQL test data.
- Coming from Faker? Read SeedBase vs Faker on why schema-aware beats per-field fakers at scale.
- Read the docs for the Laravel seeder, the
SeedbaseClientand the database connections. - Create a free account at /register to generate against your own schema.
Stop hand-writing factories and seeders.
Generate a foreign-key-consistent dataset once and seed your Laravel database with one command. Free tier, no card.
Create a free accountMore: login-free sandbox · Django · Symfony · SQL test data · vs Faker · Test data fixtures · VS Code · Docs · home
Frequently asked questions
Is it free?
Yes. Generation and JSON download are part of the free tier, so you can pull a foreign-key-consistent dataset and seed your Laravel database at no cost. Pro adds Parquet export, direct database push and higher row limits, but you do not need any of that to seed a Laravel database from your schema.
How is this different from Laravel factories and seeders?
You do not write factories or seeders per table anymore. SeedBase fills the database foreign-key-consistent out of your schema with realistic data, so an order that points at a customer always finds a customer that exists, and the values look real instead of "Premium Widget 1" or lorem ipsum.
How do I run it?
Run php artisan db:seed with the supplied SeedbaseSeeder class. The command is php artisan db:seed --class="Seedbase\Laravel\SeedbaseSeeder". It generates and loads FK-consistent data over the Laravel database connection, so your schema has to exist first, which your migrations already own.
Can I call SeedBase from my own seeder or test?
Yes. The SeedbaseClient is injectable from the container, so you can type-hint it in any seeder, command or test: public function __construct(private SeedbaseClient $seedbase) {}. Then call $this->seedbase->generate(config('seedbase.project'), ['seed' => 42, 'wait' => true]) and $this->seedbase->download($gen['id'], 'sql') to drive generation yourself instead of using the bundled seeder.
Does it replace Eloquent factories?
No, it complements them. Keep a model factory where you want one hand-built row in a focused unit test. Reach for a generated dataset when you need a whole schema filled foreign-key-consistent without writing a factory and seeder per table, because the engine resolves the relationships across tables for you instead of you wiring parents and children by hand.
Is it deterministic?
Yes, per seed. Generation is seeded, so the same seed produces the same data every run. That keeps CI reproducible: the same seeder run yields the same rows, and a failing test can be reproduced exactly by reusing the seed.
Which databases does it support?
It was tested against a real 20-app Django project with 226 tables and works multi-DB across PostgreSQL, MySQL, MariaDB, SQLite and SQL Server, which are the same connections Laravel itself drives.