Data Fixtures in Symfony2

Taylor Ren
Taylor Ren
Share

Back when I first started to learn Symfony (1.x) with its Jobeet project, I thought the ability to load a bunch of test data into the database very useful.

In this article, we will revisit this feature, which has been completely re-modeled and thus has a lot to teach us.

Preparation

In this article, we will have two 3rd party libraries to further enhance the power of Symfony.

The first is the DoctrineFixturesBundle, used to load test data with Doctrine ORM, which is the default ORM in Symfony.

Please follow the instructions in Symfony official documentation to set up and configure the DoctrineFixturesBundle. If you are familiar with Composer, the process should be easy and straightforward, no hassles.

Next, we will install PHPUnit, the default test framework used by Symfony. Its official site offers the download of the latest phpunit.phar file. Simply save that file into the Symfony root directory and we are done. Symfony has come with a default and workable PHPUnit configuration file (app/phpunit.xml.dist). In normal circumstances, we shall keep this file unchanged and PHPUnit will work fine. We’ll use PHPUnit in the followup to this article, so just make sure you have it.

Of course, to make the above two libraries work in our app, we will have to set up the Symfony project/app first. This topic is covered in my previous article “Building a Web App with Symfony 2: Bootstrapping”. Please take a look if you are not yet familiar with a Symfony 2 setup. As a matter of fact, this article is quite related to the Book Collection app we created before (with some modifications to the database schema, though).

Writing our first data fixture file

As we are building a book collection app, we need a few tables to describe the relationships among book-related information:

  • book_book: contains the information about a book, including the book id, title, author, ISBN, publisher (FK to another book_publisher table), purchase place (FK to another book_place table), purchase date, etc.
  • book_place, book_publisher: contains the information about a purchase place and a publisher, respectively.
  • book_taglist: contains the tag for a book and it forms a many-to-many relationship to book_book table.
  • book_headline, book_review: contains my reading thoughts on a particular book. Headline is the master table and forms a one-to-many relationship with book_review, a one-to-one relationship to book_book.
  • Other tables to capture additional information.

In this app, I am setting up the database (rsywx_test) with the following structure. The schema is drawn with the help of this online tool.

NOTE: I have not listed all the tables/fields in this schema drawing but just those primary keys and foreign keys to illustrate the relationships among the tables. The complete SQL dump can be found in the Github repository created for this article.

To start with, we will create the data fixture for book_place.

<?php

namespace tr\rsywxBundle\DataFixtures\ORM;

use \Doctrine\Common\DataFixtures\AbstractFixture;
use \Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use \Doctrine\Common\Persistence\ObjectManager;
use \tr\rsywxBundle\Entity\BookPlace as BookPlace;

class LoadPlaceData extends AbstractFixture implements OrderedFixtureInterface
{
    /**
     * 
     * {@inheritDoc}
     */
    public function load(ObjectManager $manager)
    {
        //Create a common place
        $place1=new BookPlace();
        $place1->setName('Common');
        $this->addReference('commonPlace', $place1);

        //Create a special place
        $place2=new BookPlace();
        $place2->setName('Special');
        $this->addReference('specialPlace', $place2);

        $manager->persist($place1);
        $manager->persist($place2);

        $manager->flush();
    }

    /**
     * 
     * {@inheritDoc}
     */
    public function getOrder()
    {
       return 2; 
    }
}

A few things to highlight here:

  1. There are 4 use statements. The first 3 are quite standard and required for almost all data fixture files. The last use is to include the namespace for table book_place. This will have to match the table we are to load data into.
  2. A data fixture file will contain a customized class (extends from AbstractFixture and implements OrderedFixtureInterface) and has at least two functions in it: load (to load the sample data) and getOrder (to specify the loading order and preserve reference integrity among PK/FK).
  3. In the load function, we create a few instances of book_place and assign the values for name. The id field is not to be assigned as it is an auto-increment field.
  4. Furthermore, after creating an object of book_place, we add a reference to this object by $this->addReference(). This is a MUST as we will need this object to be referenced again in our book_book table data loading. You may recall that in book_book table, its field place is a FK to book_place table’s id field.
  5. Finally, we persist the object and make the physical insertion into the table.
  6. In getOrder function, we explicitly indicate that this fixture file should be loaded in the 2nd place (only after book_publisher in my case).

This data fixture file should be placed under your project root/src/your bundle namespace/DataFixtures/ORM. In my vagrant Symfony setup, the directory is /www/rsywx/src/tr/rsywxBundle/DataFixtures/ORM and the file name is LoadPlace.php.

Now we can load the data fixture file:

php app/console doctrine:fixtures:load

This Symfony console command will grab all files resided in the above directory and start to insert sample data in the order specified by each file’s getOrder() function.

Done! Go back to your favorite MySQL administration tool and do a select * from book_place or alike, we will see two records have been inserted into the book_place table.

Load book data and reference to other objects

Next, let’s load book data. This fixture file will be a bit complicated as it will use a loop to create many book objects and also use previously created object reference to set the value for foreign keys.

The excerpt of LoadBook.php is as below:

<?php

namespace tr\rsywxBundle\DataFixtures\ORM;

...
use \tr\rsywxBundle\Entity\BookBook as BookBook;

class LoadBookData extends AbstractFixture implements OrderedFixtureInterface
{

    /**
     * 
     * {@inheritDoc}
     */
    public function load(ObjectManager $manager)
    {
        //Now we create a 100 general book
        for ($i = 1; $i <= 100; $i++)
        {
            $p = new BookBook();
            $p->setAuthor('Normal');
...
            $p->setPurchdate(new \DateTime());
            $p->setPubdate(new \DateTime());
...
            $p->setPage($i);
...
            $p->setPublisher($this->getReference('commonPub'));
            $p->setPlace($this->getReference('commonPlace'));
            $manager->persist($p);
        }

        //Create a special book
        $s = new BookBook();
...
        $s->setPurchdate(new \DateTime('1970-1-1'));
        $s->setPubdate(new \DateTime('1970-1-1'));
        $s->setPrintdate(new \DateTime('1970-1-1'));
...
        $this->addReference('aBook', $s);

        $manager->persist($s);

        $manager->flush();

    }

    /**
     * 
     * {@inheritDoc}
     */
    public function getOrder()
    {
        return 3;
    }

}

In this fixture file, we created a total of 101 records. One of them is special as it is created at the very end (thus having a largest id) and its date-related fields are set to EPOCH.

Notice how we set the FK fields (publisher and place). As these two fields are FK to other tables, we can’t simply assign an integer to those two. We will have to use $this->getReference() to grab the objects previously created. And YES! That is why we need getOrder() to specify the sequence of loading, i.e., book table must be loaded after publisher and place table.

Also, we created a reference for my “Special Book” so that this object can be used later on for other tables (book_headline, book_taglist).

In my app, there is a total of 6 data fixture files. After loading these 6 files, the database for my app is more or less ready. After some coding on the Controllers, Views, the site will be running:

With sample data, the site is more meaningful and can help in our further fine tuning and functional testing.

We do have a lot of functionality in this index page that is not covered in this article. For example, there are two Dart widgets showing QOTD (Quote Of The Day) and local weather information. This is covered in my previous articles in Sitepoint (Part 1 and Part 2).

We are almost done dealing with the Symfony DataFixturesBundle, save for one more thing. Recall my article on Unique Index? In a way, I am a strong protester against using auto-incremented integer fields as a table’s primary key. Also as a perfectionist, once I have created a meaningful unique index, I will simply eliminate the existence of another auto-incremented integer field and use that UI as my PK.

But to comply with the data fixture bundle’s constraint, there is one compromise to make. Symfony DataFixturesBundle says:

There must be an auto-incremented integer field as the PK to establish the relationship reference

Of course, you can create a UI and more indexes if you like.

So, in my book_book table, even though I have a self-sufficient field (book_id in the form like “12345”, a string to label my books), I have to create another field (id, auto-incremented integer) to act as the PK!

If we skip this step, the database structure itself is still valid and all FK/PK relationships can still be established. However, there will be at least one weird and hard-to-understand error: $this->getReference() in any table that references book_book (without an auto-incremented integer field as PK) will fail, prompting “Undefined index” error.

Conclusion

In this article we covered Data Fixtures, and the proper way to provide sample data to our app via Doctrine. In a followup article coming out next Monday, we’ll go through functional testing and use the data we’ve seeded here.

Feel free to comment and we will be happy to cover this topic into more detail if you are interested.

Frequently Asked Questions on Data Fixtures in Symfony2

How do I install and configure the DoctrineFixturesBundle in Symfony2?

To install the DoctrineFixturesBundle, you need to use composer. Run the command composer require doctrine/doctrine-fixtures-bundle. After installation, you need to enable the bundle in the app/AppKernel.php file of your Symfony2 project. Add the following line in the registerBundles() method: new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),. Now, the bundle is ready to use.

How can I load fixtures in Symfony2?

To load fixtures, you need to create a fixture class inside your bundle. This class should extend the Doctrine\Bundle\FixturesBundle\Fixture class and implement the load() method. Inside this method, you can create your objects and persist them using the entity manager. To load your fixtures, use the command php bin/console doctrine:fixtures:load.

What is the purpose of the getReference() method in Symfony2 fixtures?

The getReference() method is used to get a reference to an object that was previously stored using the setReference() method. This is particularly useful when you need to create relationships between your entities. You can store a reference to an object and then retrieve it in another fixture class to set a relation.

How can I order my fixture loading in Symfony2?

To order your fixture loading, you can implement the OrderedFixtureInterface in your fixture classes. This interface requires you to implement a getOrder() method that should return an integer. Fixtures are loaded in ascending order according to the value returned by this method.

Can I use fixtures for testing in Symfony2?

Yes, fixtures are a great way to set up a known state for your database before running your tests. You can load your fixtures in your test setup and then your tests can interact with a predictable set of data.

How can I load fixtures for a specific bundle in Symfony2?

To load fixtures for a specific bundle, you can use the --fixtures option followed by the path to your fixtures. For example, php bin/console doctrine:fixtures:load --fixtures=src/MyBundle/DataFixtures/ORM.

Can I use the same fixture for different environments in Symfony2?

Yes, you can use the same fixture for different environments. However, you might want to create different sets of fixtures for your development, testing, and production environments. You can specify the environment when loading your fixtures with the --env option.

How can I avoid duplicating code in my fixtures in Symfony2?

To avoid duplicating code, you can create base fixture classes or use services. You can also use the setReference() and getReference() methods to reuse entities across different fixture classes.

Can I use Faker with my Symfony2 fixtures?

Yes, Faker is a PHP library that generates fake data. You can use it in your fixtures to generate realistic data for your entities. To use Faker, you need to install it with composer and then you can use it in your fixture classes.

How can I unload or delete fixtures in Symfony2?

There is no built-in command to unload or delete fixtures. However, you can create a custom command or you can simply clear your database before loading your fixtures. Be careful when clearing your database, as it will remove all your data.