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.

Realistic, foreign-key-consistent Laravel test data generated from a schema in SeedBase

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".

Honest note: the Laravel seeder is thin, the value is the engine behind it. SeedBase was tested against a real 20-app Django project with 226 tables, that is where the foreign-key and edge-case handling came from. It works multi-DB across PostgreSQL, MySQL, MariaDB, SQLite and SQL Server, the same connections Laravel itself drives. EU-hosted, no third-party trackers, and you can export everything, so nothing is locked in.

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.

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"

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 account

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.