# Zenstruck Foundry makes Doctrine fixtures readable and on demand

> Foundry is a PHP factory library for Symfony that creates one object at a time instead of loading a fixed dataset, so a test only builds the rows it needs and every attribute is auto-completable in an editor. It works with doctrine/orm, doctrine/mongodb-odm or both, and it can run inside DoctrineFixturesBundle or inside tests.

**zenstruck/foundry** — A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.

- Repository: https://github.com/zenstruck/foundry
- Website: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html
- Stars: 801 · Forks: 114
- Language: PHP
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/zenstruck-foundry

## A factory returns one object at a time rather than a fixture dataset

The README's pitch is one line: fixtures you will actually enjoy writing. The mechanism behind it is a factory class per entity, and the flow is short enough to read in full:

```php
$post = PostFactory::new() // Create the factory for Post objects
    ->published()          // Make the post in a "published" state
    ->create([             // create & persist the Post object
        'slug' => 'post-a' // This Post object only requires the slug field - all other fields are random data
    ])
;
```

Two properties of that snippet carry the idea. The `create` call takes only the fields a test cares about, and everything else is filled with random data, so a test does not have to name every column of the entity. And states like `published` are methods on the factory, so the intent of the object is written as a call rather than as a set of magic values.

The on-demand part is the difference from the fixture loading model. The factories can be used inside DoctrineFixturesBundle to load fixtures, or inside your tests, where the documentation says it has even more features. The README also describes the system as auto-completable, which is a static analysis and IDE property: the attributes available on a factory are known to the editor.

The project is PHP, MIT licensed, on the 2.x branch, with the last push on 2026-10-08 and releases v2.14.1, v2.14.0 and v2.13.0 in the preceding weeks. The bundle documentation lives at the Symfony documentation site rather than in the repository.

## ORM, MongoDB ODM, or both in the same project

Foundry is not tied to a single persistence layer. The README states support for doctrine/orm with doctrine/doctrine-bundle, for doctrine/mongodb-odm with doctrine/mongodb-odm-bundle, or a combination of these.

The combination case is what makes this more than a one-entity-manager wrapper. A single factory can produce objects that are persisted through different managers, and the README points to the tests directory and the documentation for how that is arranged in practice.

The repository itself shows the shape of the work. At the top level there is src/ for the library, tests/, config/ for bundle configuration, stubs/, bin/, a skeleton/ directory that looks like a ready-made application for manual testing, and utils/. The configuration surface includes a .symfony.bundle.yaml file, and the project ships symfony.lock.

Static analysis is taken seriously enough to have its own set of tools in the root: phpstan.neon, psalm.xml, a .roave-backward-compatibility-check.xml file for API compatibility between releases, phpbench.json for benchmarks, and several PHPUnit configuration files including phpunit.xml.dist, phpunit-paratest.xml.dist and phpunit-9.xml.dist for older PHPUnit versions, plus phpunit-deprecation-baseline.xml and phpunit-rector.xml.dist.

The presence of a backward compatibility check and multiple upgrade guides, UPGRADE-2.0.md, UPGRADE-2.7.md and UPGRADE-2.9.md, is the signal that this is a library with a compatibility policy rather than a fast-moving internal tool.

## The factory API is borrowed from Laravel factories, and the AAA style is declared

The README credits its inspiration explicitly. The API comes from Laravel factories and from christophrumpel/laravel-factories-reloaded. That is a useful fact for a team arriving from another ecosystem, because the mental model transfers directly: named states, random data for unset attributes, and a create step that persists.

The testing style is also named. The AAA style of testing was introduced to the author by Adam Wathan's Test Driven Laravel course. That means Arrange, Act, Assert is the assumed shape of a test, and a factory call is the Arrange step.

The influences are therefore from PHP tooling rather than from Java or Python fixture libraries, which is why the library reads the way it does. The Symfony bundle documentation is the primary reference, and the README links a screencast on SymfonyCasts for a guided view.

One design consequence worth naming: because every attribute is randomly generated unless you specify it, tests stay short but you give up exact control over unspecified fields. A test that needs a specific date has to set it, and the README's example sets only the slug because that is all the test needs.

## Running Foundry's own test suite needs three database containers

The library's test suite needs one or more databases, and it ships with a docker compose configuration for that. The note in the README states that Docker and PHP installed locally, with the mysql, pgsql and mongodb extensions, are required.

The compose file defines three services. MySQL runs on image mysql:5.7 with the platform pinned to linux/x86_64 and the port mapped to 3307. Postgres runs image postgres:15 with a healthcheck of pg_isready against the zenstruck_foundry database and the port mapped to 5433. Mongo runs image mongo:4 with a healthcheck that pings the server and the port mapped to 27018.

The documented sequence to start the containers and run the suite is:

```bash
# start the container
$ docker compose up --detach

# install dependencies
$ composer update

# run main testsuite (with "schema" reset database strategy)
$ ./phpunit

# run "reset-database" testsuite
$ ./phpunit --testsuite reset-database
```

The comment in that block names the two strategies. The main testsuite uses a schema reset, and there is a separate testsuite named reset-database, which exercises the other mechanism where the database is restored through migrations. Having both means the library tests the reset path it asks users to rely on.

Note the ports. The defaults are 3307, 5433 and 27018, which are deliberately offset from the standard ports, since the containers run alongside whatever is on your machine.

## Configuration lives in .env.local, and every database can be swapped

The README documents overriding default environment variables by creating a `.env.local` file, and the point is to make permutations easy:

```dotenv
# change the database to postgreSQL...
DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15"
# ...or to SQLite
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

MONGO_URL="" # disables Mongo
USE_DAMA_DOCTRINE_TEST_BUNDLE="1" # enables dama/doctrine-test-bundle
PHPUNIT_VERSION="11" # possible values: 9, 10, 11, 11.4
```

Switching the target database is a one-line change, and blanking MONGO_URL disables Mongo entirely. That last point matters for speed, because the README notes that using both ODM and ORM makes the reset db mechanism slower.

For the faster local profile it recommends:

```dotenv
# dama/doctrine-test-bundle makes Foundry testsuite really faster
USE_DAMA_DOCTRINE_TEST_BUNDLE="1"

# using both ODM and ORM makes the "reset db" mechanism slower
MONGO_URL=""

# Use the last version of PHPUnit
PHPUNIT_VERSION="12"

# Enable all features of Foundry
USE_FOUNDRY_PHPUNIT_EXTENSION="1"
```

The maker tests are also slow, and can be skipped with a group exclusion:

```bash
$ ./phpunit --exclude-group=maker
```

Ports can be moved too. A `.env.local` can override the container port, and a docker-compose.override.yml changes the mapping, with the README giving MySQL on port 3308 as the worked example.

## The documentation is linted in a container before it merges

The last technical section of the README covers a documentation linter, and it is worth mentioning because it shows how the project treats its own docs.

The linter is contained in a Docker image, and the command to run it locally is:

```shell
docker run --rm -it -e DOCS_DIR='/docs' -v ${PWD}/docs:/docs  oskarstark/doctor-rst:latest
```

The image is doctor-rst, which checks reStructuredText syntax, and the section describes the linter as verifying that the document syntax adheres to the required standards. Running it in a container means a contributor does not need the linter installed locally.

The repository has a docs/ directory at the root, and the documentation site itself is the Symfony bundle page rather than a separate site, so the docs in this repository are the source for that page.

There is also a security policy. If you discover a vulnerability, the README asks you not to open a public issue or pull request, and instead to review the repository's Security Policy for how to report it responsibly. That is the standard arrangement for a library that many projects depend on.

The CHANGELOG.md at the root, together with the three upgrade guides, is where behaviour changes are recorded, and the CLAUDE.md file suggests the project keeps agent-oriented instructions alongside the human documentation.

## Conclusion

Adopt Foundry if your Symfony tests currently load a large fixed fixture set through DoctrineFixturesBundle and you want each test to construct exactly the objects it touches, with attributes that an editor can complete. Skip it if your suite depends on bulk data seeding, since Foundry is built around creating objects one at a time rather than loading a dataset. Verify first that your project runs on the 2.x line, that your Doctrine setup is ORM, MongoDB ODM or a combination, and that your test environment can afford the database containers the library's own suite uses.

## FAQ

### What is Zenstruck Foundry and what problem does it solve?

It is a PHP factory library for Symfony and Doctrine that creates fixtures on demand, one object at a time, with auto-completable attributes. The README's example shows that only the fields a test cares about need to be given, while the rest become random data.

### Does Foundry work with MongoDB ODM as well as the ORM?

Yes. The README states support for doctrine/orm with doctrine/doctrine-bundle, doctrine/mongodb-odm with doctrine/mongodb-odm-bundle, or a combination of the two.

### How do I run the Foundry test suite locally?

The suite needs Docker and local PHP with the mysql, pgsql and mongodb extensions. Start the containers with `docker compose up --detach`, run `composer update`, then `./phpunit` for the main testsuite with the schema reset strategy, or `./phpunit --testsuite reset-database` for the reset-database testsuite.

### How can I make the Foundry test suite run faster?

Set USE_DAMA_DOCTRINE_TEST_BUNDLE="1" and blank MONGO_URL in a .env.local file, since using both ODM and ORM slows the reset database mechanism, and skip the slow maker tests with `./phpunit --exclude-group=maker`.

## Sources

- [License: MIT](https://github.com/zenstruck/foundry/blob/2.x/LICENSE)
- [Project website](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html)
- [README](https://github.com/zenstruck/foundry/blob/2.x/README.md)
- [Releases](https://github.com/zenstruck/foundry/releases)
- [zenstruck/foundry on GitHub](https://github.com/zenstruck/foundry)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zenstruck-foundry
