Symfony test data and Doctrine fixtures, FK-consistent from your schema

Maintaining a Doctrine fixture class per entity plus the factories to wire them together rots the moment the schema moves, and copying a production dump into your test suite is a GDPR problem waiting to happen. The fix: realistic, foreign-key-consistent Symfony test data generated straight from your schema, data that actually looks real, not "Premium Widget 1" and lorem ipsum. Register one bundle, run doctrine:fixtures:load, and one command fills the whole database.

Realistic, foreign-key-consistent Symfony test data generated from a Doctrine 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.

More stacks: Laravel test data · SQL test data · Django test data

Install the Symfony bundle for Doctrine fixtures

Install the package and register the bundle, then SeedBase plugs into the fixtures workflow you already run. Pull it in with Composer:

composer require seedbase/seedbase

Register the bundle in config/bundles.php:

<?php
// config/bundles.php
return [
    // ...
    Seedbase\Symfony\SeedbaseBundle::class => ['all' => true],
];

Then configure your token, project and seed in config/packages/seedbase.yaml. The token resolves from the environment, so no secret lives in the repo:

# config/packages/seedbase.yaml
seedbase:
    token:   '%env(SEEDBASE_TOKEN)%'
    api_url: 'https://seedbase.dev/api/v1'
    project: '%env(SEEDBASE_PROJECT)%'
    seed:    '%env(int:SEEDBASE_SEED)%'

Grab a free API key under Settings, API keys (it looks like dr_sk_...) and put it in SEEDBASE_TOKEN. Create a project by importing your schema (a SQL dump, or a live database URL) at seedbase.dev and use its id as SEEDBASE_PROJECT.

Fill the database with doctrine:fixtures:load

As a DoctrineFixturesBundle alternative for the bulk of your data, you stop writing a fixture class per entity. The bundle ships a single SeedbaseFixtures class that extends the DoctrineFixturesBundle Fixture, so you load it exactly the way you already load fixtures:

php bin/console doctrine:fixtures:load

The autowired SeedbaseFixtures generates and loads foreign-key-consistent data over your Doctrine connection, using the project and seed from your config. Your schema must already exist, your migrations own it. SeedBase reads that schema, generates realistic rows that satisfy every relationship, and inserts them parent-before-child so the load succeeds with constraints on.

Inject the SeedbaseClient in a service or command

Once the bundle is registered, SeedbaseClient is autowirable. Type-hint it in any service, controller or console command and Symfony injects a configured instance, so you can drive generation from your own code: generate a dataset, then download it as SQL.

<?php
// src/Command/SeedCommand.php
namespace App\Command;

use Seedbase\SeedbaseClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: 'app:seed')]
final class SeedCommand extends Command
{
    public function __construct(private SeedbaseClient $client)
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $gen = $this->client->generate('my-project', ['seed' => 42, 'wait' => true]);
        $sql = $this->client->download($gen['id'], 'sql');

        file_put_contents('var/seed.sql', $sql);

        return Command::SUCCESS;
    }
}

generate() creates a generation and, with wait => true, blocks until it is ready, so $gen['id'] is the finished generation. download($id, 'sql') returns the INSERT statements as a string, ready to load with doctrine:database:import or pipe into psql in CI. Pass a fixed seed and the run is reproducible.

Realistic, foreign-key-consistent data

The value is the engine, not the thin Symfony adapter. SeedBase resolves foreign keys across tables, so an order that points at a customer always finds a customer that exists, and the rows load with constraints on. It uses realistic distributions and coherent free text, so a description reads like a description and an email reads like an email, instead of "Premium Widget 1" or lorem ipsum your assertions can never trust.

This complements Faker and Doctrine fixtures rather than replacing them. Faker invents one field at a time and a fixture wires the relations by hand; SeedBase invents the whole dataset schema-aware, so the foreign keys line up across hundreds of tables without you writing the wiring. Keep the bespoke fixtures that encode a specific scenario, let SeedBase fill the bulk.

Honest note: 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. The engine is multi-database: PostgreSQL, MySQL, MariaDB, SQLite and SQL Server, so the same generation seeds whichever one your Doctrine connection points at. EU-hosted, no third-party trackers, and you can export everything, so nothing is locked in.

Deterministic and reproducible

Generation is seeded. The same seed produces the same data, every run. That keeps CI reproducible: the same fixture 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 fixture that has since been hand-edited.

Free tier

Generation and SQL or JSON download are free to use, no credit card. You do not need push-to-database or any paid feature to seed your Symfony tests, because the data loads through your own Doctrine connection. Generate once, run doctrine:fixtures:load, and the bundle wires it into the database. Paid plans start at €19/month and add Parquet export, direct database push and higher row limits when you want them.

Try it

There is a runnable example you can clone and run, so you watch a real FK-consistent dataset get loaded through a Doctrine connection. Then point it at your own schema.

php examples/doctrine_seed_demo.php

Stop hand-writing Doctrine fixtures.

Generate a foreign-key-consistent dataset once and seed your Symfony database with one command. Free tier, no card.

Create a free account

Symfony test data and Doctrine fixtures: FAQ

How do I generate Symfony test data with SeedBase?

Run composer require seedbase/seedbase, register Seedbase\Symfony\SeedbaseBundle in config/bundles.php, set your token, project and seed in config/packages/seedbase.yaml, then run php bin/console doctrine:fixtures:load. The bundled SeedbaseFixtures class fills your existing schema with foreign-key-consistent rows over your Doctrine connection.

How do I replace hand-written Doctrine fixtures?

You stop writing a fixture class per entity. The single SeedbaseFixtures class reads your schema and fills the database foreign-key-consistent, with realistic distributions, so an order that points at a customer actually finds that customer and the values look real instead of Premium Widget 1 or lorem ipsum. Your migrations stay the source of truth, SeedBase only fills the schema they define.

Does it work with DoctrineFixturesBundle and Faker?

Yes. SeedbaseFixtures extends the DoctrineFixturesBundle Fixture class and loads via doctrine:fixtures:load, so it sits alongside any fixtures you keep. It complements Faker and Doctrine fixtures rather than replacing them: Faker invents one field at a time, SeedBase invents the whole dataset schema-aware and foreign-key-consistent across tables.

How do I inject the SeedbaseClient in a service or command?

Once the bundle is registered, SeedbaseClient is autowirable. Type-hint it in any service, controller or console command constructor and Symfony injects a configured instance. Call $client->generate($projectId, ['seed' => 42, 'wait' => true]) to produce a generation, then $client->download($gen['id'], 'sql') to get the SQL.

How do I configure the bundle?

In config/packages/seedbase.yaml set token to '%env(SEEDBASE_TOKEN)%', api_url to 'https://seedbase.dev/api/v1', project to '%env(SEEDBASE_PROJECT)%' and seed to '%env(int:SEEDBASE_SEED)%'. The token resolves from the config or the SEEDBASE_TOKEN environment variable, so secrets stay out of the repo.

Is the generated data deterministic for CI?

Yes, per seed. The same seed produces the same data every run, so CI is reproducible: the same fixture yields the same rows on every machine, and a failing test can be reproduced exactly by reusing the seed.

Is it free?

Yes. Generation and JSON download are part of the free tier with no credit card, so you can pull a foreign-key-consistent dataset and load it through your own Doctrine connection at no cost. Paid plans start at 19 euros per month and add Parquet export, direct database push and higher row limits, but you do not need any of that to seed your tests.