Foundry

repository·2.x·Indexed 21 days ago

https://github.com/zenstruck/foundry

An expressive, on-demand fixture creation system for Symfony and Doctrine applications. Foundry allows developers to define factories to generate objects with random or specific data for tests and fixtures, supporting doctrine/orm and doctrine/mongodb-odm drivers.

Tokens
29.5K
Snippets
87
Records
98
Agent score
73%

What's inside zenstruck-foundry

  1. Overview of Foundry

    2.x

    Foundry is an expressive, auto-completable, on-demand fixture system designed for Symfony and Doctrine. It simplifies the process of creating test data (fixtures) by integrating directly with your existing entities.

    Foundry supports:

    • doctrine/orm (requires doctrine/doctrine-bundle)
    • doctrine/mongodb-odm (requires doctrine/mongodb-odm-bundle)
    • A combination of both ORM and ODM.
  2. Reference objects using placeholders and names

    2.x

    Foundry allows you to reference objects in Gherkin steps using names (from the registry) or special placeholders.

    In Table Cells (Automatic Resolution)

    If a property type is a registered factory, you can simply use the object's name. If automatic resolution fails, use the escape hatch:

    • <foundry:object(type, name)>
    • <foundry:lastObject(type)>: References the row with the highest ID in the database.

    In Plain Step Arguments (Placeholders)

    Placeholders using the foundry: prefix work in plain text arguments (e.g., in URLs or specific step strings):

    • <foundry:id(type, name)>: The ID of a named object in the registry.
    • <foundry:lastId(type)>: The ID of the row with the highest ID in the database.
    PlaceholderWorks inResolved from
    <foundry:object(type, name)>Table cells onlyObject registry
    <foundry:lastObject(type)>Table cells onlyDatabase (highest ID)
    <foundry:id(type, name)>Plain step arguments onlyObject registry
    <foundry:lastId(type)>Plain step arguments onlyDatabase (highest ID)

    Warning: <foundry:lastId(...)> assumes IDs follow creation order (auto-increment, sequences, or time-ordered UIDs like UUID v7/ULID). For random IDs (UUID v4), use named objects with <foundry:id(factory, name)> instead.

    # Using object names in tables
    Given there is a category named "tech"
    Given there is a post named "my-post" with:
      | title   | category |
      | My Post | tech     |
    
    # Using escape hatch in tables
    Given there is a post named "my-post" with:
      | title   | category                         |
      | My Post | <foundry:object(category, tech)> |
    
    # Using lastObject in tables
    Then post named "my-post" should have properties:
      | category                       |
      | <foundry:lastObject(category)> |
    
    # Using placeholders in plain arguments (URLs)
    When I am on "/contacts/<foundry:lastId(contact)>"
    When I am on "/contacts/<foundry:id(contact, john)>"
  3. Use FactoryCollection as a PHPUnit Data Provider

    2.x

    The methods many() and sequence() on a factory return a FactoryCollection. This object can be converted into a PHPUnit data provider using the asDataProvider() method. This allows you to generate multiple sets of data for a single test method.

    Alternatively, you can pass the FactoryCollection directly as an argument to your test method to have multiple objects available within the same test case.

    use Appactory\PostFactory;
    
    /**
     * @dataProvider postDataProvider
     */
    public function test_post_via_data_provider(PostFactory $factory): void
    {
        $factory->create();
    }
    
    public static function postDataProvider(): iterable
    {
        yield from PostFactory::new()->sequence(
            [
                ['title' => 'foo'],
                ['title' => 'bar'],
            ]
        )->asDataProvider();
    }
  4. Understand Factory immutability

    2.x

    Factories in Foundry are immutable. Methods that modify the factory state (like with(), instantiateWith(), beforeInstantiate(), afterInstantiate(), or afterPersist()) do not modify the existing object but instead return a new factory instance.

    use Appactory\PostFactory;
    
    $factory = PostFactory::new();
    $factory1 = $factory->with([]); // returns a new PostFactory object
    $factory2 = $factory->instantiateWith(function () {}); // returns a new PostFactory object
  5. Use In-Memory Repositories for DDD/Hexagonal testing

    2.x

    Foundry (v2.5+) supports experimental "in-memory" behavior. This allows you to use in-memory implementations of your domain repositories instead of Doctrine-backed ones during tests.

    Implementation Steps:

    1. Create an In-Memory Repository: Implement Zenstruck\Foundry\InMemory\InMemoryRepository and use the InMemoryRepositoryTrait. The repository must implement a _class() method returning the entity class it manages.
    2. Configure the Container: Ensure your in-memory repository is used in the container (e.g., via a test-in-memory environment or an InMemoryKernel).
    3. Use the #[AsInMemoryTest] Attribute: Apply this attribute to your KernelTestCase. This disables factory persistence and allows you to register an "after instantiate" hook to store objects in your in-memory repositories.

    When using this mode, YourFactory::repository() returns the in-memory repository, and no database queries are made.

    use App\Domain\Address\DomainAddressRepositoryInterface;
    use Zenstruck\Foundry\InMemory\InMemoryRepository;
    use Zenstruck\Foundry\InMemory\InMemoryRepositoryTrait;
    
    /**
     * @implements InMemoryRepository<Address>
     */
    final class InMemoryAddressRepository implements InMemoryRepository, DomainAddressRepositoryInterface
    {
        use InMemoryRepositoryTrait;
    
        public static function _class(): string
        {
            return Address::class;
        }
    }
    
    #[AsInMemoryTest]
    final class SomeTest extends KernelTestCase
    {
        private InMemoryAddressRepository $addressRepository;
    
        protected function setUp(): void
        {
            $this->addressRepository = self::getContainer()->get(InMemoryAddressRepository::class);
        }
    
        #[Test]
        public function object_should_be_accessible_from_in_memory_repository(): void
        {
            $address = AddressFactory::createOne();
            self::assertSame([$address], $this->addressRepository->_all());
            
            // No database query is made here
            self::assertSame(1, AddressFactory::repository()->count(1));
        }
    
        protected static function getKernelClass(): string
        {
            return InMemoryKernel::class;
        }
    }
  6. Features of the Foundry PHPUnit extension

    2.x

    When the Zenstruck\Foundry\PHPUnit\FoundryExtension is installed, you gain access to the following features:

    • Global Foundry Bootstrapping: Automatically boots Foundry, removing the need to manually use the Factories trait in your test cases.
    • Automated Database Reset: Enables automated mechanisms for resetting the database between tests.
    • #[WithStory] Attribute Support: Provides support for the #[WithStory] attribute.
    • Data Provider Support: Allows the use of Factory::create() within PHPUnit Data Providers (requires PHPUnit ^11.4).
  7. Choose the correct Factory base class in Foundry 2.0

    2.x

    In Foundry 2.0, Zenstruck\Foundry\ModelFactory is deprecated. You must choose one of the following based on your use case:

    • \Zenstruck\Foundry\ObjectFactory: Use for creating plain objects that are not persisted.
    • \Zenstruck\Foundry\Persistence\PersistentObjectFactory: Use for ORM entities or ODM documents that you want to persist and return directly. Note: This does not return a Proxy; remove calls to ->object() on objects created this way.
    • \Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory: Use for ORM entities or ODM documents when you want to leverage "auto refresh" behavior. This acts most like the old ModelFactory.

    Factory Method Changes:

    • getDefaults(): array $\rightarrow$ defaults(): array|callable
    • getClass(): string $\rightarrow$ public static function class(): string
    • initialize() $\rightarrow$ protected function initialize(): static
  8. Create Non-persisted objects

    2.x

    To create objects that do not interact with the database (useful for unit tests or non-entity objects), you can use two approaches:

    1. Inherit from Zenstruck\Foundry\ObjectFactory: This creates plain objects.
    2. Use withoutPersisting(): For existing persistent factories, call withoutPersisting() before creating.

    To make a factory non-persisting by default, override its initialize() method.

    Manual Persistence: If an object is created without persisting, you can use the save($object) helper to persist it later.

    use App\Entity\Post;
    use App\Factory\PostFactory;
    use function Zenstruck\Foundry\Persistence\save;
    
    // Using an existing factory without persisting
    $post = PostFactory::new()->withoutPersisting()->create();
    $post->setTitle('something else');
    save($post); // Persist manually
    
    // Using the convenience helper
    $entity = object(Post::class, ['field' => 'value']);
  9. Create Anonymous Factories and Repositories

    2.x

    If you don't have an explicit factory class for an entity, you can use persistent_factory() to create an anonymous factory. To interact with the data created by these factories, use the repository() helper.

    Anonymous Factory API:

    • $factory->create(['field' => 'value'])
    • $factory->many(5)->create(['field' => 'value'])
    • $factory->instantiateWith(function () {})
    • $factory->beforeInstantiate(function () {})
    • $factory->afterInstantiate(function () {})
    • $factory->afterPersist(function () {})

    Repository API:

    • $repository->first(['createdAt']): Get the first object (or latest if a column is provided).
    • $repository->last(['createdAt']): Get the last object (or oldest if a column is provided).
    • $repository->truncate(): Empty the database table.
    • $repository->count(): Number of persisted objects.
    • $repository->all(): Returns all persisted objects.
    • $repository->findBy(['field' => 'value']): Returns matching objects.
    • $repository->find($id) or $repository->find(['field' => 'value']): Find by ID or filter.
    • $repository->random(['field' => 'value']): Get a random object.
    • $repository->randomSet(count, ['field' => 'value']): Get a random set of objects.
    • $repository->randomRange(start, end, ['field' => 'value']): Get a random range of objects.
    use App\Entity\Post;
    use function Zenstruck\Foundry\Persistence\persistent_factory;
    use function Zenstruck\Foundry\Persistence\repository;
    
    $factory = persistent_factory(Post::class);
    $factory->create(['field' => 'value']);
    
    $repository = repository(Post::class);
    $repository->first();
    $repository->findBy(['author' => 'kevin']);
  10. Define and use reusable factory states

    2.x

    States allow you to define specific configurations for your objects as reusable methods. Because factories are immutable, you must chain states off of the $this object returned by the state method.

    To use states, use the new() method to instantiate the factory, chain your state methods, and finally call create() to persist the object.

    // Inside the Factory class
    public function published(): self
    {
        return $this->with(['published_at' => self::faker()->dateTime()]);
    }
    
    public function withViewCount(?int $count = null): self
    {
        return $this->with(function () use ($count) {
            return ['view_count' => $count ?? self::faker()->numberBetween(0, 10000)];
        });
    }
    
    // Usage in tests
    $post = PostFactory::new()->published()->withViewCount(10)->create();
  11. What are Stories and how to use them

    2.x

    Stories are used to extract a specific database state into a reusable unit. They are ideal when the arrange step of a test becomes complex or when you need to duplicate complex fixture logic between tests and development environments. Stories can be loaded in tests, fixtures, or even depend on other stories.

    Key characteristics:

    • They can be loaded once per test execution; subsequent calls to load() do nothing.
    • Objects persisted in stories are cleared after each test (unless using a Global State Story).
    • They can be defined as services to allow dependency injection.
    // Load the state defined in the story's build() method
    PostStory::load();
    
    // Subsequent calls do nothing
    PostStory::load();
  12. Use Auto-Refresh for factory objects

    2.x

    Auto-refresh allows factory-created objects to automatically reflect changes made to the database during a functional test (e.g., via an HTTP request).

    Requirements: Requires PHP 8.4+ to leverage lazy objects. For older versions, Foundry uses a Proxy mechanism (which is deprecated).

    To enable it, set enable_auto_refresh_with_lazy_objects: true in your Foundry configuration.

    #[ResetDatabase]
    class MyTest extends WebTestCase
    {
        public function test_with_autorefresh(): void
        {
            $post = PostFactory::createOne(['title' => 'My Title']);
    
            $client = self::createClient();
            $client->request('GET', "/update-post/{$post->id}", ['title' => 'New Title']);
            
            self::assertResponseIsSuccessful();
    
            // The object is automatically refreshed from the DB
            $this->assertSame('New Title', $post->getTitle());
        }
    }