Skip to content

Instantly share code, notes, and snippets.

@nikophil
Last active January 22, 2026 18:17
Show Gist options
  • Select an option

  • Save nikophil/c76162e8ebfc362385664b2f485e6c60 to your computer and use it in GitHub Desktop.

Select an option

Save nikophil/c76162e8ebfc362385664b2f485e6c60 to your computer and use it in GitHub Desktop.
Foundry's constitution

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

The file ".specify/memory/constitution.md" MUST also be loaded in memory and is VERY important.

Language Preference

Always respond in French (Français).

About This Project

Zenstruck Foundry is a Symfony bundle that provides an expressive, auto-completable, on-demand fixtures system for Doctrine ORM and MongoDB ODM. It allows developers to create test data using a fluent factory API that can be used in both tests and data fixtures.

Development Commands

Running Tests

# Start required services (MySQL, PostgreSQL, MongoDB)
docker compose up --detach

# Install dependencies
composer update

# Run main test suite
./phpunit

# Run specific test suites
./phpunit --testsuite main                    # Unit tests only
./phpunit --testsuite reset-database          # Database reset strategy tests

# Run single test
./phpunit path/to/TestFile.php --filter testMethodName

# Exclude slow maker tests
./phpunit --exclude-group=maker

Fast Local Testing

Create .env.local with these settings for 3-5x faster tests:

USE_DAMA_DOCTRINE_TEST_BUNDLE="1"  # Wraps tests in transactions
MONGO_URL=""                        # Skip MongoDB if not testing it
PHPUNIT_VERSION="12"                # Use latest PHPUnit
USE_FOUNDRY_PHPUNIT_EXTENSION="1"  # Enable Foundry's PHPUnit extension

Database Configuration

Override database in .env.local:

# PostgreSQL
DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15"

# SQLite (no Docker needed)
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

Code Quality

# Static analysis
bin/tools/phpstan/vendor/phpstan/phpstan/phpstan analyse

# Psalm (for generated factories)
bin/tools/psalm/vendor/vimeo/psalm/psalm

Important: Ne jamais lancer php-cs-fixer manuellement sur ce projet. Le formatage du code est géré automatiquement par la CI.

Architecture Overview

Factory Hierarchy

The core factory system uses three levels of abstraction:

Factory (abstract)
  ├─ ObjectFactory (abstract)
  │    └─ PersistentObjectFactory (abstract)
  │         └─ PersistentProxyObjectFactory (abstract, deprecated in 2.7+)
  └─ ArrayFactory

Key files:

  • src/Factory.php - Base factory with immutable builder pattern and attribute normalization
  • src/ObjectFactory.php - Adds object instantiation, hooks (beforeInstantiate, afterInstantiate), and object reuse
  • src/Persistence/PersistentObjectFactory.php - Adds database persistence, repositories, and afterPersist hooks

Key concept: All factory methods return cloned instances (immutable builder pattern), enabling safe method chaining:

PostFactory::new()
    ->published()           // Returns new clone
    ->with(['title' => 'Foo'])  // Returns new clone
    ->create()

Persistence Architecture

The persistence layer uses an adapter pattern to abstract Doctrine ORM and MongoDB ODM:

PersistentObjectFactory
  → ProxyGenerator (lazy loading via Symfony VarExporter)
  → PersistenceManager (central orchestrator)
  → PersistenceStrategy (adapter interface)
      ├─ OrmV2PersistenceStrategy / OrmV3PersistenceStrategy
      └─ MongoPersistenceStrategy

Key files:

  • src/Persistence/PersistenceManager.php - Orchestrates persistence operations
  • src/Persistence/ProxyGenerator.php - Generates lazy-loading proxies using eval() and Symfony VarExporter
  • src/Persistence/PersistenceStrategy.php - Abstract adapter for Doctrine backends
  • src/ORM/AbstractORMPersistenceStrategy.php - Doctrine ORM implementation
  • src/Mongo/MongoPersistenceStrategy.php - MongoDB ODM implementation

Proxy pattern: Objects are wrapped in lazy-loading proxies that:

  • Defer object creation until first property/method access
  • Auto-refresh from database when accessed (configurable)
  • Provide methods like _save(), _refresh(), _delete(), _assertPersisted()

Configuration & Test Integration

Configuration (src/Configuration.php):

  • Singleton that boots Foundry with factories, faker, and persistence managers
  • Manages global state for factory registry, stories, and instantiator
  • Supports in-memory mode for unit tests (no database required)

Test integration:

  • PHPUnit: src/Test/Factories.php trait boots Foundry, src/PHPUnit/FoundryExtension.php manages lifecycle
  • Behat: src/Test/Behat/FoundryExtension.php integrates with Symfony kernel
  • In-memory mode: src/InMemory/ provides database-less testing for unit tests

Bundle (src/ZenstruckFoundryBundle.php):

  • Symfony bundle that registers services, configures DI container
  • Configures faker locale/seed, instantiator defaults, database reset strategies
  • Registers maker commands and test extensions

Key Patterns

Immutable builder: Factory methods return cloned instances, never mutating the original.

Hook system with priorities: Callbacks at instantiation and persistence points:

  • beforeInstantiate(callable $callback, int $priority = 0) - Modify parameters before object creation
  • afterInstantiate(callable $callback, int $priority = 0) - Post-process object after creation
  • afterPersist(callable $callback, int $priority = 0) - Final customization after database persistence

Lazy loading: ProxyGenerator creates runtime proxy classes using Symfony's VarExporter and eval(), wrapping real objects for deferred instantiation.

Object reuse: reuse(object ...$objects) composes objects via type-based injection into factory attributes.

FactoryCollection: Batch operations via many(), range(), sequence(), and distribute() methods.

Relationship system: src/Persistence/Relationship/ handles bidirectional relationships (ManyToOne, OneToMany, OneToOne) to maintain consistency.

Special Development Considerations

Multiple PHPUnit Versions

The ./phpunit script dynamically manages PHPUnit versions (9, 10, 11, 12) based on PHPUNIT_VERSION env var:

PHPUNIT_VERSION="11" ./phpunit

Different PHPUnit versions require different configuration files and extension loading strategies.

Database Reset Strategies

Schema reset (default, faster):

./phpunit --testsuite reset-database
# Uses: doctrine:schema:drop + doctrine:schema:update

Migrate reset (tests real migrations):

# In .env.local:
DATABASE_RESET_MODE="migrate"
MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php"

./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php

Performance Optimization

DAMA/DoctrineTestBundle: Wraps tests in database transactions for 3-5x speedup (automatically rolls back after each test):

USE_DAMA_DOCTRINE_TEST_BUNDLE="1" ./phpunit

PHP 8.4 Lazy Objects: Alternative to proxy generation using native PHP 8.4 lazy objects:

# config/packages/zenstruck_foundry.yaml
zenstruck_foundry:
    enable_auto_refresh_with_lazy_objects: true

Maker Tests

Maker tests generate factories and verify output against expected files. They're slow, so exclude them for faster iteration:

./phpunit --exclude-group=maker

Expected output files are in tests/Fixture/Maker/expected/.

Key File Locations

Core factory system:

  • src/Factory.php, src/ObjectFactory.php, src/Persistence/PersistentObjectFactory.php
  • src/FactoryCollection.php - Batch operations

Persistence layer:

  • src/Persistence/PersistenceManager.php, src/Persistence/ProxyGenerator.php
  • src/Persistence/PersistenceStrategy.php - Adapter interface
  • src/ORM/ - Doctrine ORM adapters (v2 and v3)
  • src/Mongo/ - MongoDB ODM adapter

Test integration:

  • src/Test/Factories.php - PHPUnit trait
  • src/PHPUnit/FoundryExtension.php - PHPUnit extension
  • src/Test/Behat/ - Behat integration
  • src/InMemory/ - In-memory repositories for unit tests

Maker commands:

  • src/Maker/MakeFactory.php - Factory generator
  • src/Maker/MakeStory.php - Story generator
  • src/Maker/Factory/ - Factory generation logic

Bundle:

  • src/ZenstruckFoundryBundle.php - Symfony bundle
  • src/DependencyInjection/ - Service configuration

Object instantiation:

  • src/Object/Instantiator.php - Multiple instantiation strategies
  • src/Object/Hydrator.php - Property setting via PropertyAccess

Additional Notes

  • The codebase supports both Doctrine ORM and MongoDB ODM simultaneously
  • Factories can be used in DoctrineFixturesBundle fixtures AND in tests
  • The bundle provides repository decorators (ProxyRepositoryDecorator) with test assertions
  • Stories are fixture sets that can be loaded in tests via $this->load(UserStory::class)
  • The functions.php files provide global helper functions (e.g., factory(), repository())
  • add phpdoc @internal for classes that must not be directly used by final users
  • for string concatenation, if no static involved, prefer interpolation to sprintf
  • utilise le moins possible de commentaires, uniquement la phpdoc qui est utile et qui n'est pas un doublon des déclaration de type
  • tous les fichiers de code modifiés doivent l'être en anglais, sauf éventuellement les specs et les plans
  • à chaque fois que Claude pose des questions, il doit toujours proposer un onglet supplémentaire pour donner des directives qui n'ont potentiellement rien à voir avec les questions en cours
  • minimise les variables temporaires inutiles
  • préfère les array_map() et array_filter() au foreach, sauf que c'est trop complexe
  • préfère les nouvelles méthodes array_find(), array_find_key(), array_any() et array_all() sur les tableaux de PHP 8.4 et 8.5 aux foreach, sauf que c'est trop complexe
  • si possible, les callable doivent être static

grepai - Semantic Code Search

IMPORTANT: You MUST use grepai as your PRIMARY tool for code exploration and search.

When to Use grepai (REQUIRED)

Use grepai search INSTEAD OF Grep/Glob/find for:

  • Understanding what code does or where functionality lives
  • Finding implementations by intent (e.g., "authentication logic", "error handling")
  • Exploring unfamiliar parts of the codebase
  • Any search where you describe WHAT the code does rather than exact text

When to Use Standard Tools

Only use Grep/Glob when you need:

  • Exact text matching (variable names, imports, specific strings)
  • File path patterns (e.g., **/*.go)

Fallback

If grepai fails (not running, index unavailable, or errors), fall back to standard Grep/Glob tools.

Usage

# ALWAYS use English queries for best results (--compact saves ~80% tokens)
grepai search "user authentication flow" --json --compact
grepai search "error handling middleware" --json --compact
grepai search "database connection pool" --json --compact
grepai search "API request validation" --json --compact

Query Tips

  • Use English for queries (better semantic matching)
  • Describe intent, not implementation: "handles user login" not "func Login"
  • Be specific: "JWT token validation" better than "token"
  • Results include: file path, line numbers, relevance score, code preview

Call Graph Tracing

Use grepai trace to understand function relationships:

  • Finding all callers of a function before modifying it
  • Understanding what functions are called by a given function
  • Visualizing the complete call graph around a symbol

Trace Commands

IMPORTANT: Always use --json flag for optimal AI agent integration.

# Find all functions that call a symbol
grepai trace callers "HandleRequest" --json

# Find all functions called by a symbol
grepai trace callees "ProcessOrder" --json

# Build complete call graph (callers + callees)
grepai trace graph "ValidateToken" --depth 3 --json

Workflow

  1. Start with grepai search to find relevant code
  2. Use grepai trace to understand function relationships
  3. Use Read tool to examine files from results
  4. Only use Grep for exact string searches if needed

Foundry Constitution

Core Principles

I. Immutable Builder Pattern (NON-NEGOTIABLE)

All factory methods MUST return cloned instances, never mutating the original factory. This enables safe method chaining and prevents unexpected side effects.

Rules:

  • Every method that modifies factory state MUST use clone $this before applying changes
  • Factory instances MUST be reusable without side effects
  • Method chaining MUST be predictable and composable

Rationale: The immutable builder pattern is the architectural foundation of Foundry's API. Breaking this pattern would cause subtle, hard-to-debug issues for all users.

II. Developer Experience First

The API MUST be expressive, auto-completable, and discoverable. Developer experience takes priority over internal implementation simplicity.

Rules:

  • Public API methods MUST have clear, intention-revealing names
  • Return types and parameters MUST enable IDE auto-completion
  • Error messages MUST clearly indicate what went wrong and how to fix it
  • New features MUST integrate seamlessly with existing factory patterns

Rationale: Foundry's value proposition is making fixtures "fun again." Every API decision should be evaluated through the lens of developer joy.

III. Persistence Agnosticism

Foundry MUST support multiple persistence backends. New features MUST be agnostic and not coupled to any specific persistence system (Doctrine ORM, MongoDB ODM, or future backends).

Rules:

  • New persistence features MUST use the PersistenceStrategy adapter pattern
  • Core factory logic MUST NOT depend on Doctrine-specific concepts
  • Backend-specific code MUST be isolated in dedicated strategy implementations
  • When a feature cannot be implemented for a backend, it MUST fail gracefully with a clear message
  • Tests MUST run against all supported backends when persistence is involved

Rationale: Users choose their persistence layer based on their domain needs. Foundry should never force a persistence technology choice and must remain open to future backends.

IV. Backward Compatibility

Breaking changes to public API MUST be avoided in minor versions. Deprecations MUST provide migration paths for at least one major version.

Rules:

  • Public API signatures MUST NOT change in minor/patch versions
  • Deprecated features MUST trigger clear deprecation warnings with migration instructions
  • New features SHOULD use optional parameters or new methods rather than changing existing signatures
  • Major version upgrades MUST include comprehensive UPGRADE guides
  • Exception: Classes and methods marked @internal MAY change without BC considerations

Rationale: Foundry is used in test suites that run frequently. Unexpected breakages to public API waste developer time and erode trust.

V. Test-Driven Quality

All new code MUST be covered by tests. Static analysis MUST pass before merging.

Rules:

  • New features MUST include test coverage for happy paths and edge cases
  • PHPStan analysis MUST pass at the configured level
  • Behat tests MUST pass for behavior-related changes
  • Tests MUST run with multiple PHPUnit versions (9, 10, 11, 12) to ensure compatibility
  • Tests MUST run against both Doctrine ORM and MongoDB ODM where applicable
  • code MUST be compatible with PHP 8.1
  • Exception: some functions from PHP 8.4 and PHP 8.5 can be used because we have a polyfill

Rationale: Foundry helps users write tests. It would be ironic—and dangerous—if Foundry itself had poor test coverage.

Development Standards

Language Requirements

  • Source code MUST be written in English (variable names, class names, method names)
  • PHPDoc and code comments MUST be in English
  • Technical documentation (docs/index.rst) MUST be in English
  • Claude specification documents (specs.md, plans.md, tasks.md) MUST be in French
  • PHPDoc SHOULD only be added when it provides information beyond type declarations
  • Comments SHOULD be minimal; prefer self-documenting code

Code Style

  • String concatenation SHOULD use interpolation rather than sprintf() when no static analysis benefit
  • Classes not intended for public use MUST be marked with @internal
  • Useless temporary variables SHOULD be avoided
  • préfère les array_map() et array_filter() au foreach, sauf que c'est trop complexe
  • préfère les nouvelles méthodes array_find(), array_find_key(), array_any() et array_all() sur les tableaux de PHP 8.4 et 8.5 aux foreach, sauf que c'est trop complexe
  • si possible, les callable doivent être static
  • php-cs-fixer MUST NOT be run manually - code formatting is handled automatically by CI

API Design

  • Public methods MUST use explicit parameter types and return types
  • Factory methods MUST return static to enable proper inheritance
  • Hooks (beforeInstantiate, afterInstantiate, afterPersist) MUST support priority ordering
  • Collections MUST support many(), range(), sequence(), and distribute() patterns

Quality Gates

Before Merge

  • All PHPUnit tests pass (./phpunit)
  • All Behat tests pass (./vendor/bin/behat)
  • PHPStan passes (bin/tools/phpstan/vendor/phpstan/phpstan/phpstan analyse)
  • New code has test coverage
  • Documentation updated if public API changed
  • Code compatible with PHP 8.1

For Major Changes

  • Tested with both Doctrine ORM and MongoDB ODM (change MONGO_URL in .env.local)
  • Backward compatibility verified for public API (not required for @internal)
  • UPGRADE guide updated if breaking changes introduced

Governance

This constitution supersedes all other development practices for the Foundry project. All pull requests and code reviews MUST verify compliance with these principles.

Amendments:

  • Constitution changes require documentation and explicit approval
  • Amendments MUST include a migration plan for existing code if applicable
  • Version history MUST be maintained in this document

Compliance:

  • Use CLAUDE.md for runtime development guidance and commands
  • Refer to this constitution for architectural decisions and trade-offs

Version: 1.0.0 | Ratified: 2025-01-14 | Last Amended: 2025-01-14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment