How to Seed Data With Symfony Hautelook Fixtures

Zlatko-Spajic.jpeg
Symfony-data-seed.jpg

HautelookAliceBundle is a tool you can use to effortlessly seed test data for your development environment. The bundle utilizes a PHP library called Faker which generates the test data for you.

Many times when we are developing a project, we waste lots of time by filling our database with test data. This is sometimes done manually by writing queries or just manually inserting data through our application. 

That usually involves a tedious process of registering a user, logging in as that user and adding test data. Often, we recreate the database and have to repeat the process over and over again. This data often ends up being gibberish and it is hard to read.

In this article, we'll go through the whole process of automating this and creating test data that actually makes sense.

It's pretty straightforward. In the past, seeding data wasn’t quite possible because in complex data structures seeding breaks down when we have related tables. Basically, there wasn't even a tool to help developers get started with seeding related data.

The biggest problem was with ordering data. Sometimes, there are many relations and it is hard to keep track of all the foreign keys so that we can insert the data properly. It's challenging and you can spend a lot of time when you have more entities in your app. Now it’s much easier to resolve it with DoctrineFixturesBundle. But now comes something even more helpful for you to manage and seed fake data.

Fortunately, there's a great wrapper for DoctrineFixturesBundle called Hautelook/Fixtures - a third-party bundle that will greatly help with your fixtures management. Hautelook fixtures is a powerful tool to manage fixtures with nelmio/alice and fzaninotto/Faker. This extension allows us to persist the loaded fixtures and we don’t need to take care of which data will be seeded first. The database support is done in FidryAliceDataFixtures. In this blog, we will explain how to seed data into many to many relations.

Technical Requirements

Before creating your first Symfony application, you have to install:

  • PHP 7.4
  • Symfony 5 or above
  • Composer 2.1.5
  • MySQL or PostgreSQL

Getting started

We will be creating an app that will help us track blood donors.
Use the following commands to create your project:

                                composer create-project symfony/skeleton q_agency_blood_donation
                            

Doctrine already comes as standard ORM with Symfony, so we'll assume you already have this covered. If not, take a look at the docs HERE.

                                composer require doctrine-orm
                            

Additionally to speed up the development process we are going to install a Symfony helper library called MakerBundle. This bundle is the fastest way to generate the most common code you'll need in a Symfony app: commands, controllers, form classes, event subscribers and more:

                                composer require --dev symfony/maker-bundle
                            

A Symfony bundle to manage fixtures with Alice and Faker which allows you to create a ton of fixtures/fake data:

                                composer require hautelook/alice-bundle
                            

After the installation is finished, take a look in the config/bundles.php file. The core features of Symfony framework are enabled for any environment while our installed third-party bundles are enabled for dev and test. So we will use the dev environment for local development:

                                <?php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
    Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true],
    Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true],
    Hautelook\AliceBundle\HautelookAliceBundle::class => ['dev' => true, 'test' => true],
];
                            

Dependencies are prepared, let’s configure a database.You can find and customize this in the .env file. Just uncomment the environment variable called DATABASE_URL and set your database name, user and password:

                                DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
                            

Now that your connection parameters are set up, use this command to create the db in your relational database management system:

                                php bin/console doctrine:database:create
                            

In this example, we will show how fixtures work and what you can do. The Idea of this project is to manage related data with foreign keys. The feature of our application is that we have all the necessary information with just a few tables.

diagram.png
Visualized diagram of our database

Since this blog is about properly seeding our development environment, I will leave a challenge for the reader to create the entity classes.
You can create classes standalone following doctrine documentation. So your next job after the classes are made is to create entities and make migration with the command:

                                php bin/console make:migration
                            

Let’s create some fixtures

After creating your database, we are ready for development. Our next step is to configure the hautelook alice yaml file:

                                
hautelook_alice:
    fixtures_path: fixtures

There is a parameter for our path and it will load all the fixtures found in the fixtures folder in which we will define our dummy data. If you want, you can change your fixtures path:
Then, in your root folder create a fixtures folder. There we will create and define our yaml files for seeding.

First, we are creating eight different blood groups: 

                                
App\Entity\BloodGroup:
  bloodGroup_{1..8}:
    name (unique): <bloodGroup()>

How? Don’t worry about it, faker/generator has implemented methods for blood groups.

To understand how fixtures work, look at the BloodGroup structure of the tree above:

  • The first node presents entity of object
  • The second node reference object instance, range in bracket, presents number of objects
  • And the last define property and its method

Unique means that all those groups will be different.

Now let’s create donor, a ten donors to be exact:

                                
App\Entity\Donor:
  donor_{1..10}:
    firstName: <firstName()>
    lastName: <lastName()>
    bloodGroup: '@bloodGroup_*'

As you can see, donor_ {1..n} defines how many donor objects you will create. Methods firstName() and lastName are implemented in the faker generator class. Please pay attention to the bloodGroup property: @bloodGroup is a reference to the bloodGroup object defined in bloodGroup.yaml, and it calls the magic method get() and sets it randomly as foreign key created in the previous BloodGroup.yaml file.

Now, we have to make bloodBank.yaml. In this example, we added a parameter as an addition to the email property:

                                
parameters:
  email_param: red.cross

App\Entity\BloodBank:
  bloodBank_{1..5}:
    name (unique): <company()>
    email: '<{email_param}>'
    phone: <phoneNumber()>
    address: <address()>

After creating the bloodBank.yaml file, it’s time to create blood donation events:

                                
App\Entity\BloodDonation:
  bloodDonation_{1..3}:
    bloodBank: '@bloodBank_*'
    location: <city()>
    date: <datetimeBetween('-50 days', 'now')>

At the end we need to define the donorDonation.yaml file. Take another look at our database diagram.

Let's explain our ‘many to many’ relationship.

One donor can have multiple blood donations, and one blood donation can have multiple donors. To avoid this problem, we created a third table with extra properties like blood amount and transaction success. This new table breaks the many to many into two one to many relations.

As you can see, there are multiple definitions of the object. You can create more different references, but they all need to have the same root entity:

                                
App\Entity\DonorDonation:
  donorDonation_{1..9}:
    bloodDonation: '@bloodDonation_1'
    donor:  '@donor_<current()>'
    amount: <randomFloat(1,0,0.8)>
    success: <boolean()>
    #createdAt: <datetimeBetween('-50 days', 'now')>
  donorDonation_10:
    bloodDonation: '@bloodDonation_2'
    donor: '@donor_1'
    amount: <randomFloat(1,0,0.8)>
    success: <boolean()>
    #createdAt: <datetimeBetween('-50 days', 'now')>
  donorDonation_11:
    bloodDonation: '@bloodDonation_2'
    donor: '@donor_2'
    amount: <randomFloat(1,0,0.8)>
    success: <boolean()>
    #createdAt: <datetimeBetween('-50 days', 'now')>
  donorDonation_{12}:
    bloodDonation: '@bloodDonation_3'
    donor: '@donor_2'
    amount: <randomFloat(1,0,0.8)>
    success: <boolean()>
    #createdAt: <datetimeBetween('-50 days', 'now')>

When we are done, we can try to seed these fixtures:

                                php bin/console hautelook:fixtures:load
                            

You can always try this command when you make your first fixture yaml file. There is no reason to wait for it all to be finished. Every time you run this command, it will revert all your data. Besides, nothing will go wrong. Just be brave.

Remark

If you want to try to seed data without a processor, to avoid error, you should uncomment all createdAt properties in DonorDonation.yaml.

Processor

Processors allow you to process objects before and/or after they are persisted. Just create a new class. It doesn't matter where it goes because the processor implemented the processor interface. And that means we have to have two methods: postProcess() and preProcess()


Here is a preProcess example for a DonorDonation entity:

                                
<?php

namespace App\Processor\Fixtures;

use App\Entity\DonorDonation;
use Fidry\AliceDataFixtures\ProcessorInterface;


class DonorDonationProcessor implements ProcessorInterface
{
    public function preProcess(string $id, $object): void
    {
        if (!$object instanceof DonorDonation) {
            return;
        }
        $object->setCreatedAt(new \DateTime());
        $amount = (float)$object->getAmount();
        $success = $this->donationSuccess($amount);

        $object->setSuccess($success);

    }

    public function postProcess(string $id, $object): void
    {

    }

    public function donationSuccess(float $amount): bool
    {
        return $amount > 0;
    }
}

You shouldn’t have all the properties defined in the fixtures yaml file. For example, in DonorDonation.yaml, the createdAt property is commented. This property is setter in the preProcess method. Another example in this class is the success of transactions. A custom method is created to check if a donor is donating blood.

Open your database and check donor_donation and blood_donation tables. Donor donation belongs to blood donation. However, you can see that related tables don't match the same dates because the datetimeBetween method is randomly selected. To fix it, we will use one brilliant idea. In the postProcess method, we will get the date of the BloodDonation object and set this date to the donorDonation:

                                
<?php

namespace App\Processor\Fixtures;

use App\Entity\BloodDonation;
use App\Entity\DonorDonation;
use Doctrine\ORM\EntityManagerInterface;
use Fidry\AliceDataFixtures\ProcessorInterface;

class BloodDonationProcessor implements ProcessorInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {

        $this->entityManager = $entityManager;
    }

    public function preProcess(string $id, $object): void
    {

    }

    public function postProcess(string $id, $object): void
    {
        if (!$object instanceof BloodDonation) {
            return;
        }

        $id = $object->getId();
        $date = $object->getDate();

        /** @var DonorDonation $donorDonation */
      $donorDonations =  $this->entityManager->getRepository(DonorDonation::class)->findBy(['bloodDonation' => $id]);

      foreach ($donorDonations as $donorDonation)
      {
          $donorDonation->setCreatedAt($date);

          $this->entityManager->persist($donorDonation);

      }

        $this->entityManager->flush();

    }
}

We finally got to the end

You can use the --purge-with-truncate option to switch from DELETE to a TRUNCATE purge mode. It will depend on your DBMS to clear auto-increment settings on that action:

                                php bin/console hautelook:fixtures:load --purge-with-truncate
                            

If you followed all these steps exactly as described, you can execute your own query or copy this one below. Open your database CLI and run the query:

                                
SELECT bg.name as BloodGroup, count(d.id) as Donors_number, ROUND(SUM(dd.amount),2) AS BloodSum from donor_donation dd 
LEFT JOIN donor d on dd.donor_id = d.id 
LEFT JOIN blood_group bg on d.blood_group_id = bg.id 
GROUP BY bg.id ORDER BY bg.name

And blood donation statistics will be displayed.

check-data.png

Or open your database visual tool to check data.

Conclusion

Instead of using the real data from the production database, it's common to use fake data in the test database. We used methods implemented in faker class. With hautelook fixtures you can easily manage data and seed it without wasting your time.

After this blog, you won’t insert data manually anymore. I hope that this article will help you improve your skills and that in future, you will use this amazing tool for seeding data. You can find and clone project source code on GitHub repository.

Thanks for reading!