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.
First, create your Doctrine entities.
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, extendsZenstruck\Foundry\ModelFactory
class. But it’s not a big deal if you don’t.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) usingCustomerFactory
and, for each customer, create one or several addresses (Address
entities) usingAddressFactory
.To get automatic access to the bundle’s
Voltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService
service, extend your story class fromVoltel\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-hintVoltel\ExtraFoundryBundle\Service\FixtureEntity\EntityProxyPersistService
in the__construct
method of your story class).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 byFactory: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.