How we built almost all our dev/test instantiations around Zenstruck Foundry

From Static PHP Files to Living, Typed, and Malleable Code.
Philosophy & Background
- Zenstruck Foundry: A PHP package designed for easy data creation, essential for dev and test environments. It is heavily compatible with Symfony and Doctrine
- Factory : A PHP class that generates and builds objects using our Doctrine entities.
- Fixture (Reminder): Datasets, typically dummy / fake data, injected into the database or handled within a transaction.
Tools & defintion
Traditional Entity

Factories - `defaults()` - Public Mutators

Usage Example

Development-ready fixtures
# bin/console foundry:load-fixtures report --append

Stories & Scenarios for Reusable Data

Stories & Doctrine Fixtures together


# bin/console doctrine:fixtures:load --group=rpsi
Foundry's Core Logic: The Bottom-Up Approach
Laboratory (Root, the bottom)
└── Mission ──> [Linked to a Laboratory]
└── Dossier ──> [Linked to a Mission]
└── Seal ──> [Linked to a Dossier - Seals (many)]
└── SealObject ──> [Linked to a Seal] (the top)
What will be happen on Foundry when we call SealObjectFactory::new() to create a SealObject ?
The Cascade Engine: Visualizing the Flow
The Cascade Engine: Visualizing the Flow
-
The PHP Discovery (Bottom-Up)
-
SealObjectchild requires a parentSeal. -
Sealrequires a parentDossier... up to the rootLaboratory. -
Foundry climbs the tree to build the memory blueprint.
-
-
The SQL Insertion (Top-Down)
-
Database constraints (
foreign_key) rule the world. -
Doctrine reverses the order: it inserts the
Laboratoryfirst, captures its ID, and cascades down back to yourSealObject.
-

No more "mysterious database" artifacts.
A disposable and repeatable environment, managed directly by our Factories.
Data as Code
Foundry's API prioritizes creating child relations before their parents.
Graph Instantiation
Any Factory will automatically handle scalar data and nested entity associations in the exact right order.
TL;DR
No more "mysterious database" artifacts.
A disposable and repeatable environment, managed directly by our Factories.
Data as Code
Foundry's API prioritizes creating child relations before their parents.
Graph Instantiation
Advanced Features & Power User Tips
Batch-based incremental data generation.
Sequences
& distribute
Overriding private attributes and sharing persisted objects across factories to simulate scenarios.
Force
Using afterInstantiate to handle complex post-construction states.
Hooks & Init

`afterInstantiate($obj)` hook
Using afterInstantiate for inverse relationships when the owning side lacks a setter.


Overriding Private Attributes
Setting a predictable state on entities designed without public setters (immutability)



Force private relations on factory


Key-Based Object State (Stories)


Sequence & Distribute

Methods used to generate a data matrix with as many rows

Domain-Driven Providers
PoliceEmailProvider
@interieur.gouv.fr
ChemicalSubstanceProvider
Generating real-world molecules instead of generic strings.
Domain-Specific Randomization (Custom Faker)
Custom Providers: generating randomized yet realistic and valid data


Using the Provider

Ubiquitous Factories: Foundry Everywhere
Testing Strategy: DB Integration vs. No DB (Pure Unit Tests)
The goal : Foundry shouldn't be restricted to database tests.
We leverage its features across all testing levels
Integration / Kernel Tests (The Standard Way)
-
Uses a real database connection.
-
Entities are fully persisted, and IDs are naturally generated by the DB.
-
Perfect for testing real repositories, services, workflows etc
Unit Tests
-
No database persistence (
isPersisting() === false) -
Objects are instantiated in-memory, but still get unique IDs seamlessly.
-
Useful to use our Factory for testing domain logic with an Entity full hydrated without booting a Kernel.
Under the Hood: Smart In-Memory Auto-IDs
If it's a unit test, Foundry automatically disable the persistance useful to use a magic method : withAutoId()
It calls the Foundry’s force() helper to inject IDs directly into the id private property on the AbstractFactory.
Factory::new()->create() stays exactly the same, clean, and database-free on unit test.
::new() / LazyValue() / ::randomOrCreate() VS Create
Performance
Performance Trick: new()+ LazyValue vs. create()
-
Using LazyValue on the factory will lazily load data without persisting it right away ("until I need it").
-
"new" then "create" allows Foundry to build the entire graph in-memory first.
-
One Single Request: Everything is committed to the database in one single batch/flush operation.
Performance Trick: new()/random()+ LazyValue vs create()

Performance Trick: new()/random()+ LazyValue vs. create()

flush_after() ensuring internal entity modifications are fully committed

The Factory Mantras: Go Lazy, Flush Wisely
The Dark Side : Pitfalls & Debugging
Tweak the foundry initialization is needed to have a full entity with an ID on unit test.
Doctrine Identity
The silent explosion of INSERT queries during poorly managed cascade creations.
Query Count
The debugging nightmare when it masks the true origin of a Doctrine error.
"Rainbow Sparkles"
The Collection Nightmare: Inverse Relationships
-
The Solution: "in the between" with
afterInstantiate-
We defer the collection generation until the parent object is fully instantiated.
-
This ensures the
entityexists (and has its ID, whether from the DB or our in-memory auto-ID).
-



Another solution : a method to fully create the Entity on the inverse and call it on a custom Instantiator


Conclusion: One Tool, Two Essential Worlds
This use of Foundry is unifying our data generation.
Special thanks to Kevin Bond and Nikophil for this tool that changed the way we code.
https://github.com/zenstruck/foundry
https://github.com/nikophil
https://github.com/kbond
https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html
Thanks for the tool
Foundry
By Florian CELLIER
Foundry
- 18