Symfony Fixtures: smart way to create dummy data

Testing websites without proper data is very hard (if not impossible) and creating meaningful data can be time-consuming if you are not using some kind of automation for this process.

These automations could save you hours of work. One of the most popular methods for the Symfony framework is to use the data fixtures bundle that helps you create objects and store them to the database programmatically. Once you write the code for fixtures, you can load them by using a single command – php bin/console doctrine:fixtures:load.

It’s easy to use as it doesn’t have many restrictions, which is great for fast development, but things could get messy on bigger projects if you are not careful. I decided to share a few mistakes I made when I started using this bundle and what you could do to avoid them.

First of all, for a class to become a fixture, it needs to extend the Doctrine\Bundle\FixturesBundle\Fixture and implement the load method.

Suppose we are creating fixtures for an entity called Team, the class would look something like this:

<?php
namespace App\DataFixtures;
use App\Entity\Team;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class TeamFixture extends Fixture
{
     public function load(ObjectManager $manager)
     {
         $backendTeam = new Team();
         $backendTeam->setName('backend');     
         $manager->persist($backendTeam);     
         $manager->flush(); 
     }
}

If you have the maker bundle, you can run the make:fixture command that will generate an empty fixture class with a placeholder for the load method. After loading the fixtures, we will have the backend team saved in our database.

1. Separate fixtures into multiple files

Each entity should have its own fixture class (in its own file, of course) like DeveloperFixtures, TeamFixtures and so on. Entities and relationships between them are usually simple at the beginning but get more complex as the project grows. It may seem like over-engineering when you start, but separating could be tricky later, so it’s best to go with this approach from the start.

2. Prefer dependency over order

Let’s say we also have a Developer entity and we want to assign each developer to a team. This means we will need to create teams before developers, or in terms of fixtures – the team fixtures need to be loaded first, so we could use them in developer fixtures. One way to this is to implement OrderedFixtureInterface, a method getOrder() that has to return a number. The fixtures will now be executed in the ascending order of the value returned by getOrder(). Although this may seem handy, as we add more fixtures, we can notice this is very hard to maintain.

If we would want to add new fixtures in the middle of that order, we would have to update the order for all fixtures that need to load later. A better way of achieving dependency is to implementing the DependentFixtureInterface. This will require us to add the getDependencies() method to our DeveloperFixtures class – it will return an array of class names of the fixtures it depends on:

public function getDependencies(): array
{
     return [
         App\DataFixtures\TeamFixtures::class,
     ];
}

3. Fetch object through the reference repository

One of my favorite features in this bundle is that the fixture classes can keep track of all objects that are created in the current run of load command. This allows you to use the same objects across multiple fixtures, without accessing the database.

In the example with teams and developers, in the TeamFixtures you would need to add a line of code that sets the reference:

$backendTeam = new Team();
$backendTeam->setName('backend');
/**
 * this is where we set the refence
 */
$this->addReference('team.backend', $backendTeam); 

… and then fetch the same reference in the DeveloperFixtures:

/**
 * this is the object we created in TeamFixtures
 */
$backendTeam = $this->getReference('team.backend'); 
$john = new Developer();
$john->setTeam($backendTeam);
$john->setFirstName('john'); 
$this->addReference('developer.john', $john);

4. Keep your data meaningful

Although the use of lorem ipsum is widely excepted, fixtures should be as close to real data as possible. Creating developers with names dev1, dev2 and dev3 will not give as much context in testing as will using real names like John, Eve and Charles. With real names, you will get a much clearer view of how your app will look like in production.

The easiest way to keep your data meaningful is by using Faker – a bundle specialized for providing random real-life examples for names, addresses, sentences, files, dates and everything else one could need when creating mock data. Besides this, the bundle offers a great set of additional features like localization, generating multiple objects at once and seeding data.

In our example, we could update the code to add more details to our Developer using Faker:

$john = new Developer();
$john->setTeam($backendTeam); 
$john->setFirstName($this->faker->firstName);
$john->setLastName($this->faker->lastName);
$john->setEmail($this->faker->email);
$john->setLanguage($this->faker->languageCode);
$john->dateOfBirth($this->faker->dateTimeThisCentury);
$john->setExpirence($this->faker->numberBetween(0, 30));
$john->isActive($this->faker->boolean(90));
$this->addReference('developer.john', $john);

5. Create entities in batches

As you can see in the example above, we are not setting any fields manually, so nothing is preventing us from using this code to create multiple developers. We can just wrap the code for creating a developer in a for loop and get, for example, 100 developers with real-life data in our database.

for ($i = 0; $i < 100; ++$i) {
     
     $developer = new Developer();
     $developer->setTeam($backendTeam); 
     $developer->setFirstName($this->faker->firstName); 
     $developer->setLastName($this->faker->lastName); 
     $developer->setEmail($this->faker->email); 
     $developer->setLanguage($this->faker->languageCode); 
     $developer->dateOfBirth($this->faker->dateTimeThisCentury);       
     $developer->setExpirence($this->faker->numberBetween(0, 30));  
     $developer->isActive($this->faker->boolean(90));
}

To get a reference to one of these objects in other fixtures, use:

$randomReferenceKey = sprintf('developer.%d', $this->faker->numberBetween(0, 99));
$this->getReference($randomReferenceKey);

If you keep these few pieces of advice in mind, you can easily get a bunch of meaningful and maintainable data. Fixtures are not hard to use and you will be happy to find that you have more time to deal with more interesting challenges than creating dummy data.