Creating Doctrine entities populated with fake data

Luino, Italy

Creating database fixtures in projects using doctrine/orm can be cumbersome - but it doesn’t have to be.

When you create and populate entities with fake data by hand, your tests can quickly become bloated and difficult to understand. In need of a cure, you might extract helper methods or classes. The chances are that use a third-party library for setting up test fixtures.

At the time of writing, doctrine/data-fixtures and nelmio/alice are by far the most-downloaded packages for creating and loading fixtures for Doctrine entities, with 33 million and 11 million total downloads, respectively.

I have used both of them, but there are a few things that bother me.

The primary issue I have with these packages is that there is a considerable disconnect between entities created in a fixture and those used in a test. Developers can establish a connection loosely, using hard-coded values, or indirectly, using constants. I have found tests using these fixtures hard to understand and difficult to maintain.

breerly/factory-girl-php

In 2014, a colleague recommended breerly/factory-girl-php, a port of Ruby’s factory_bot to PHP, and I have been a big fan since.

The big difference is that instead of creating fixtures, with breerly/factory-girl-php, I can use a fixture factory to create entity definitions. Based on these definitions, I can then use the fixture factory to create entities. Depending on the nature of the entity definitions, the fixture factory will populate the entities with fake data. Entities can have references to other entities, and the fixture factory establishes associations to these automatically. Similar to the other packages, I can create entities for demonstration or testing purposes. However, the connection between the test code and entities is immediate.

I have enjoyed working with breerly/factory-girl-php so much that I started contributing to it. To simplify working with entity definitions, I built ergebnis/factory-girl-definition, a companion that allows loading entity definitions from a directory. In March 2020, I sent an email to Grayson Koonce, the owner of breerly/factory-girl-php. I wondered if he would accept me as a collaborator.

ergebnis/factory-bot

Since I never got a reply, I decided to split and started working on ergebnis/factory-bot.

Splitting ergebnis/factory-bot from breerly/factory-girl-php allows me to work on my terms, make my own decisions, and, more importantly, will enable me to move faster.

Since splitting, I have removed code that I never needed and modernized the codebase. I have integrated ergebnis/factory-girl-definition, added features, and foremost, documentation and examples that help you get started.

You can install ergebnis/factory-bot by running

$ composer require --dev ergebnis/factory-bot

The fixture factory requires an instance of Doctrine\ORM\EntityManagerInterface (for reading class metadata from Doctrine entities, and for persisting Doctrine entities when necessary) and an instance of Faker\Generator for generating fake data.

<?php

use Doctrine\ORM;
use Ergebnis\FactoryBot\FixtureFactory;
use Faker\Factory;

$entityManager = ORM\EntityManager::create(...);
$faker = Factory::create(...);

$fixtureFactory = new FixtureFactory(
    $entityManager,
    $faker
);

With the fixture factory set up, you can create definitions for Doctrine entities.

<?php


use Ergebnis\FactoryBot\Count;
use Ergebnis\FactoryBot\FieldDefinition;
use Ergebnis\FactoryBot\FixtureFactory;
use Example\Entity;

/** @var FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\User::class, [
    'avatar' => FieldDefinition::reference(Entity\Avatar::class),
    'id' => FieldDefinition::closure(static function (Generator $faker): string {
        return $faker->uuid;
    }),
    'location' => FieldDefinition::optionalClosure(static function (Generator $faker): string {
        return $faker->city;
    }),
    'login' => FieldDefinition::closure(static function (Generator $faker): string {
        return $faker->userName;
    }),
]);

$fixtureFactory->define(Entity\Avatar::class, [
    'height' => FieldDefinition::closure(static function (Generator $faker): int {
        return $faker->numberBetween(300, 600);
    }),
    'url' => FieldDefinition::closure(static function (Generator $faker): string {
        return $faker->imageUrl();
    }),
    'width' => FieldDefinition::closure(static function (Generator $faker): int {
        return $faker->numberBetween(400, 900);
    }),
]);

With the fixture factory aware of entity definitions, you can now create entities populated with fake data.

<?php

use Ergebnis\FactoryBot\Count;
use Ergebnis\FactoryBot\FieldDefinition;
use Ergebnis\FactoryBot\FixtureFactory;
use Example\Entity;

/** @var FixtureFactory $fixtureFactory */
$user = $fixtureFactory->createOne(Entity\User::class, [
    'login' => FieldDefinition::value('localheinz'),
]);

var_dump($user->location()); // `null` or random city
var_dump($user->login());    // 'localheinz'

$users = $fixtureFactory->createMany(
    Entity\User::class,
    Count::between(0, 10),
    [
        'login' => FieldDefinition::sequence('user-%d'),
    ]
);

var_dump($users); // array with 0-10 instances of Entity\User

I have released ergebnis/factory-bot:0.2.0 yesterday, and you can take a look at the documentation and examples, and try it out today.