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)

    • SealObject child requires a parent Seal.

    • Seal requires a parent Dossier... up to the root Laboratory.

    • 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 Laboratory first, captures its ID, and cascades down back to your SealObject.

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 entity exists (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

Made with Slides.com