Data Fixtures in Symfony2

Taylor Ren

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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Xavier Herriot

    You can use the option “–purge-with-truncate” to restart your auto increment from 1, usefull in some cases.

  • Herberto Graça

    Hi, could you give an example of setting up fixtures for a many-to-many relationship?

    tkx

    • Taylor Ren

      I paste the code here. Basically, it is similar in my case (many books – many tags):

      public function load(ObjectManager $manager)

      {

      //Create a few tags

      for ($i=1;$isetTag(“tag$i”);

      $t->setBid($this->getReference(‘aBook’));

      $manager->persist($t);

      }

      $manager->flush();

      }

  • Taylor Ren

    Thanks.

    This new Alice fixture seems nice. Care to write something on it to promote this to all?

    Not agree on “overkill” though.

  • Taylor Ren

    Good to know. Thanks.