Seeding the development database with fixture entities

The recommended way to seed the database with sample data while developing is to use DoctrineFixturesBundle. To install Doctrine Fixture bundle, follow the instructions given on DoctrineFixturesBundle page of Symfony docs.

Steps to follow to seed the database

In general, the steps required to seed the development database with entities aren’t much different with VoltelExtraFoundryBundle from when you use vanilla zenstruck/foundry bundle.

The difference is mostly with the implementation of stories (classes extending from Zenstruck\Foundry\Story). VoltelExtraFoundryBundle offers a EntityProxyPersistService (described in this chapter) and SetUpFixtureEntityService (described in “How to set up custom entities in a test” section) which make populating a database easy to code, clear to read and fast to execute.

  1. First, create your Doctrine entities.

  2. Then, for each entity, create a corresponding custom factory class extending Zenstruck\Foundry\ModelFactory as described in Model Factories.

    For reasons described elsewhere, it is recommended to extend Voltel\ExtraFoundryBundle\Foundry\Factory\AbstractFactory class which, in its turn, extends Zenstruck\Foundry\ModelFactory class. But it’s not a big deal if you don’t.

  3. Use your custom factories inside your custom story classes as described in Stories.

    For example, inside the CustomerStory class, you can create a batch of customers (Customer entities) using CustomerFactory and, for each customer, create one or several addresses (Address entities) using AddressFactory.

    To get automatic access to the bundle’s Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService service, extend your story class from Voltel\ExtraFoundryBundle\Foundry\Story\AbstractStory class.

    Otherwise, use usual Symfony’s dependency injection tricks to get access to the persist service inside your story class extending the Zenstruck\Foundry\Story class (e.g. type-hint Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService in the __construct method of your story class).

  4. Use your custom stories inside Doctrine Fixture classes.

    So, your custom Fixture class extending from Doctrine\Bundle\FixturesBundle\Fixture may look like this:

    class DataFixture extends Fixture
    {
        public function load(ObjectManager $manager)
        {
              ProductStory::load();
              // see example of CustomerStory class below
              CustomerStory::load();
              OrderStory::load();
        }
    }
    

    For other examples of Using with DoctrineFixtureBundle read zenstruck/foundry docs.

Create entities with delayed persist/flush

Create separate entities with delayed persist/flush

Use Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService::createOne() method to create a single entity.

Pass Zenstruck\Foundry\Factory object as the first parameter. In the second optional parameter, you can pass either an array of attributes for the factory, or a callback function to return an array of attributes as described in Attributes section of zenstruck/foundry docs.

The last (third) parameter is optional as well. It may accept an array with names of the factory states (i.e. names of the factory class methods, as described in Reusable Model Factory “States”).

Note

States in the final parameter can be presented as just state names for methods that do not require arguments, or an associative array elements where an array key is the state/method name and an array value is the method argument(s).

If a state method has several arguments, they should be listed in an array (see “state three” in the example below). If a state method has only one argument, it can be presented w/o a wrapping array (see “state_four” in the example below):

// array representing states for "createOne()" and "createMany()" methods
[
    'state_one',
    'state_two',
    'state_three' => ['argument_1', 'argument_2'],
    'state_four' => 10,
]
// in CustomerStory.php

use Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService;
use Zenstruck\Foundry\Story;

class CustomerStory extends Story
{
    private $persistService;

    /**
     * If your story class extends
     * "Voltel\ExtraFoundryBundle\Foundry\Story\AbstractStory"
     * the "EntityProxyPersistService" will be injected automatically
     * with standard Symfony configuration
     */
    public function __construct(EntityProxyPersistService $persistService)
    {
        $this->persistService = $persistService;
    }


    public function build(): void
    {
        $this->createCustomer();
    }


    private function createCustomer()
    {
        // Create a factory that won't immediately persist
        $customer_factory = CustomerFactory::new()->withoutPersisting();
        //
        // Although not explicitly used here, "withoutPersisting()"
        // will be automatically invoked for this factory
        // in persistService before creating entities
        $address_factory = AddressFactory::new();

        for ($i = 0; $i < 20; $i++) {
            $n_address_count = rand(1, 3);

            $this->createCustomerWithAddresses($customer_factory, $address_factory, $n_address_count);
        }

        // Persist and flush new entities as a batch operation.
        // This takes less time than persisting/flushing each entity
        $this->persistService->persistAndFlushAll();
    }


    private function createCustomerWithAddresses(
        CustomerFactory $customer_factory,
        AddressFactory $address_factory,
        int $n_address_count = 1
    )
    {
        /** @var Customer $customer_entity */
        $customer_entity = $this->persistService->createOne($customer_factory);

        for ($i = 0; $i < $n_address_count; $i++) {
            /** @var Address $address_entity */
            $address_entity = $this->persistService->createOne($address_factory);

            $address_entity->setCustomer($customer_entity);
            //
            // or use a specialized collection manipulation method
            // $customer_entity->addElementToAddressCollection($address_entity);

        }
    }

}
The EntityProxyPersistService:createOne() method is responsible for several things:
  • Applying some custom factory states to the factory stub, if provided. An array of state names is optional and can be passed as the third (last) argument.

  • Factory will be cloned to avoid immediate persistence, i.e. Zenstruck\Foundry\Factory::withoutPersisting() method is going to be invoked.

  • An array of optional arguments for new entities, if provided, will be directly passed as an argument to Zenstruck\Foundry\Factory::create() method.

  • A new Zenstruck\Foundry\Proxy object returned by Factory:create() will be internally put in a “proxy jar” to be later persisted by corresponding entity manager. Persisting entities in batches speeds up the whole process.

Create batches of entities with delayed persist/flush

Use Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService::createMany() method to create several entities at once. Similar to EntityProxyPersistService::createOne(), pass the factory stub as the first argument and the number of entities to create as the second argument.

This method may conveniently be modified with the third parameter, that can accept either an array of attributes or, more importantly, a callback that returns an array of attributes. This enables random values for every of the created entities.

You can find examples of using a callback with createMany() in several sections of the zenstruck/foundry docs, e.g. in Using with DoctrineFixtureBundle and Many-To-One sections. Here is an example from test suite of this bundle:

// in OrderStory.php

private function createOrderItemsForOrder(Order $order_entity)
{
    $factory_order_item = OrderItemFactory::new()
        //->withoutPersisting()
        ->forOrder($order_entity);

    // This will create a batch of OrderItem entities,
    // each with its unique "unitCount" value
    //
    $this->persistService->createMany($factory_order_item, rand(1, 4), function() {
        return [
            'unitCount' => rand(1, 20)
        ];
    });
}

I refer to the first argument as a “factory stub” because that the factory can be further modified by passing the fourth (last) argument – an array of state names (states) or a callback returning such an array.

States are just method names in the model factory class that will be invoked on the factory with the result that the factory will be cloned with new attributes as described in Reusable Model Factory “States”.

Setting relationships between entities

The zenstruck/foundry library makes it easy to create related entities of @ORM\ManyToOne associations right from inside the factory class (i.e. by providing a factory for a particular entity property, as described under TIP 2 in Many-To-One section).

// in OrderItemFactory.php

protected function getDefaults(): array
{
    $faker = self::faker();

    return [
        'notes' => $faker->realText(),

        // To randomly assign a product for this order item.
        // Products must be seeded in the database
        // before orders and order items.
        'product' => ProductFactory::repository()->random(),
    ];
}

The same is possible for Many-To-Many relationship.

I prefer to have this logic outlined inside a story, where related entities are created and referenced explicitly:

// in ProductStory.php

private function createProduct(ProductFactory $factory, int $n_entity_count = 20)
{
    $repo_category = CategoryFactory::repository();

    for ($i = 0; $i < $n_entity_count; $i++) {
        /** @var Product $product */
        $product = $this->persistService->createOne($factory);

        // Add from 1 to 3 categories to each Product
        // "Product" and "Category" have a Many-To-Many relationship
        $a_category_proxies = $repo_category->randomRange(1, 3);

        foreach ($a_category_proxies as $oThisCategoryProxy) {
            /** @var Category $oThisCategoryEntity */
            $oThisCategoryEntity = $oThisCategoryProxy->object();

            $product->addElementToCategoryCollection($oThisCategoryEntity);
        }
    }
}

The example above could be rewritten to be more succinct, as described in Many-To-Many section:

// in ProductStory.php

private function createProduct(ProductFactory $factory, int $n_entity_count = 20)
{
    $repo_category = CategoryFactory::repository();

    $this->persistService->createMany($factory, $n_entity_count, function() use ($repo_category) {
        return [
            'categoryCollection' => $repo_category->randomRange(1, 3),
        ];
    });

}

Note

In the code example above, if there is no “setter” for “categoryCollection” property, the factory should use a custom instantiator to “force-set” it.

This can only be a solution for unidirectional associations like in this example, where Product holds a unidirectional @ORM\Many-To-Many association with Category entities.

For bidirectional associations, you will most likely need a more sophisticated setter that will establish the opposite side of the relationship. For example, one Customer can be related to many Address entities, and each Address entity is related to one Customer (bidirectional @ORM\One-To-Many associations). In this case, the setter for Customer::addressCollection property could look like this:

// in Customer.php

/**
 * @param Address[]|null $addresses
 * @return Customer
 */
public function setAddressCollection(?array $addresses): Customer
{
    $this->addressCollection = new ArrayCollection($addresses);
    foreach ($addresses as $address) {
        $address->setCustomer($this);
    }
    return $this;
}

Alternatively, you could use a specialized collection manipulation method similar to the addElementToCategoryCollection() method (usage is shown in the example above):

// in Customer.php

public function addElementToAddressCollection(Address $address)
{
    if ($this->addressCollection->contains($address)) return;
    $this->addressCollection->add($address);
    $address->setCustomer($this);
}

Using factory states to populate arrays and establish relationships between entities

While Reusable Model Factory “States” is a great way to set model attributes in a more explicit way in terms of readability, with zenstruck/foundry it is not yet possible to manipulate array values, particularly, to add individual values to arrays using states.

If you extended your factory class from Voltel\ExtraFoundryBundle\Foundry\Factory\AbstractFactory class, you will have a AbstractFactory::addValuesTo() method at your disposal. This method can be used to do exactly what it says: add values to an array stored in a custom model attribute.

Let’s see an example (it can be found in a Voltel\ExtraFoundryBundle\Tests\Setup\Factory\ProductFactory class):

// in ProductFactory.php

public function car(): self
{
    return $this->addState([
        // Note: this will add two values to existing values of "categories" attribute
        'categories' => $this->addValuesTo('categories', ['car', 'vehicle']),
    ]);
}

As ModelFactory::addState() method will create a clone of current factory, normally, the state that modifies some attribute will override all previous values, a behavior that is not sometimes desirable. So, AbstractFactory::addValuesTo() method will take the previous value of the attribute with the name passed in the first argument, and modify it to be an array holding all previous values and the new values, passed in the array in the second argument.

Imagine, you modified your ProductFactory with two states: car and luxury:

The luxury state is similar to the car state and might look like this:

// in ProductFactory.php

public function luxury(): self
{
    return $this->addState([
        // Note: this will add a "luxury" value to existing values of "categories" attribute
        'categories' => $this->addValuesTo('categories', ['luxury']),
    ]);
}

With this setup, by the time you instantiate your Product entity, the attributes will look like this:

// in ProductFactory.php

protected function initialize()
{
    return $this
        ->instantiateWith((new Instantiator())
            ->allowExtraAttributes(['categories'])
        )
        ->beforeInstantiate(function($attributes) {
            // $attributes['categories'] => ['luxury', 'car', 'vehicle']
            return $attributes;
        })

Then, in the afterInstantiate callback, you can find those specific Category entities in the database and assign them to the Product:

// in ProductFactory.php

protected function initialize()
{
    // ...

    $this->afterInstantiate(function(Product $product, $attributes) {
        // If explicit category names were assigned by factory states,
        // find related categories and assign to the product
        if (!empty($attributes['categories'])) {
            foreach ((array) $attributes['categories'] as $c_this_category) {
                $category_proxy = CategoryFactory::findOrCreate([
                    'categoryName' => $c_this_category
                ]);

                /** @var Category $category_entity */
                $category_entity = $category_proxy->object();

                $product->addElementToCategoryCollection($category_entity);
            }
        }
    });

    // ...

    return $this;
}

When your setup will do just fine with random categories assigned to Product entities, there are obviously simpler ways to fetch random Category entities and set them on Product::categoryCollection. But when you need some specific product categories, moving this logic from stories into a model factory itself feels like a better alternative, and using states for this task makes it even more elegant. With just one line of code, you can create a batch of entities and establish some of the relationships “in one go”.

// in story or test class

public function createLuxuryCars()
{
    $factory_product_stub = ProductFactory::new();

    // create 20 Product entities in categories "luxury", "car" and "vehicle" with random "productName"
    $this->persistService->createMany($factory_product_stub, 20, function(Generator $faker) {
        return [
            'productName' => $faker->randomElement([
                '2021 Porsche Boxster', '2021 Genesis G80', '2021 Volvo S90', '2021 BMW 7 Series',
                '2021 Chevrolet Corvette', '2021 Audi TT', '2021 Audi A5', '2020 Mercedes-Benz SL',
                '2021 Genesis G90', '2020 Kia K900', '2020 Mercedes-Benz E-Class',
                '2020 Audi R8', '2020 Mercedes-Benz S-Class',
            ]),
        ];
    }, ['luxury', 'car', 'recent', 'promoted']);

    $this->persistService->persistAndFlushAll();
}

Note

In the example above, states recent and promoted will modify model attributes (registeredAt and inPromotion, respectively), and states luxury and car will add values to a custom attribute categories which is used in ProductFactory::afterInstantiate() callback to find related Category entities in the database and assign them to the products.