This guide is for the SQL version of Phactory. You can also view the MongoDB Phactory guide.

Contents

Installation and Setup

Refer to the download page for instructions on installing Phactory for PHP through Pear or to download the source code.

Once you’ve installed Phactory, you’re ready to start using it as a database factory in your unit tests. Whether you use PHPUnit, SimpleTest, or something else entirely, Phactory is easy to get started with.

To include the Phactory library in your test suite, just use the following statement in your Bootstrap or individual test file:

require_once 'Phactory/lib/Phactory.php';

Follow the rest of this guide to learn how to use Phactory for database test object creation in your unit tests, and make use of some of the more advanced features. You’ll find that Phactory is a lot easier and more flexible than traditional database fixtures.

Creating a Connection

Phactory uses PDO to perform all database operations. That means you’ll need to pass a PDO object to Phactory before you can start using it. Currently, Phactory supports only MySQL and SQLite.

To set Phactory’s database connection:

$pdo = new PDO('mysql:host=127.0.0.1; dbname=testdb', 'test_user', 'test_password');
Phactory::setConnection($pdo);

Defining Table Blueprints

Before you can create an object with Phactory, you have to define a blueprint for the table you want to use. The define() function takes a singular table name and an optional array of default values. These defaults will be set on each object you create in this table, unless you override them at creation time.

Assuming you have a table called ‘users’ in your database with columns ‘name’ and ‘age’, the following will define a blueprint for that table:

Phactory::define('user', array('name' => 'test_user', 'age' => 20));

After this, every time you create a user object with Phactory it will have a name of ‘test_user’ and an age of 20 (unless you override these values).

Creating Objects

Once you have a database connection and you’ve defined a blueprint, you’re ready to create objects with Phactory.

Let’s assume you’ve got a database with a ‘users’ table, and you’ve already setup Phactory with a PDO database connection. The columns on our ‘users’ table are ‘id’, ‘name’, and ‘activated’. Here’s an example of how you might define and create some users:

Phactory::define('user', array('activated' => 0));

$john = Phactory::create('user', array('name' => 'John Doe'));
$joe  = Phactory::create('user', array('name' => 'Joe Smith', 'activated' => 1));
$anon = Phactory::create('user');

After this code runs, $john, $joe and $anon are all Phactory_Row objects, and a row for each has been inserted into the ‘users’ table. We didn’t specify an ‘id’ for any of them because we assume that the primary key for the table is AUTOINCREMENT.

Notice that when we created $john and $joe we overrode at least one of the default values by passing an array as the second argument to create().

$john looks like this:

$john->id == 1;
$john->name == 'John Doe';
$john->activated == 0;

$joe looks like this:

$joe->id == 2;
$joe->name == 'Joe Smith';
$joe->activated == 1;

And $anon looks like this:

$anon->id == 3;
//$anon->name will be whatever the default value for the 'name' column is in your db
$anon->activated == 0;

Getting an Object

With a valid database connection, Phactory can also retrieve table rows as objects. This works much like the create() method without the need to define a blueprint. So let’s say we want to get a user out of the table which was not added by Phactory. As long as we know some information about that user, we can simply use the get() method. If the user’s name is ‘David Moore’ and we are certain the name column in our table contains unique values or we know the user’s id , we can do the following:

$david = Phactory::get('user', array('name' => 'David Moore'));
$joe = Phactory::get('user', array('id' => 2));

If you don’t know a unique identifier for the row you want, you can add more key value pairs to the second argument to make sure you get the correct one:

$joe = Phactory::get('user', array('name' => 'Joe Smith', 'age' => 20, 'activated' => 1));

Another use for get() is when you want to be certain that a row containing selected values exists in the table. If we wanted to be certain that ‘John Doe’ from earlier had his ‘activated’ column value set to 0 we could write:

if(Phactory::get('user', array('name' => 'John Doe'))->activated === 0){
	print("John Doe is not active");
}

Sequences

Phactory provides the ability for your default values to contain an automatically-incrementing sequence. If you use this functionality, each successive object you create in a table will have a different value even if you don’t override the default.

To use a sequence, simply put $n in the value you pass to define(). The $n will be substituted with a number that starts at 0 and is incremented by 1 each time you create an object in that table.

For example:

Phactory::define('user', array('name' => 'user$n', 'age' => '$n'));

$user0 = Phactory::create('user'); // name == 'user0', age == '0'
$user1 = Phactory::create('user'); // name == 'user1', age == '1'
$user2 = Phactory::create('user'); // name == 'user2', age == '2'

Be careful when using sequences with string interpolation (i.e. double quotes). The following will not create a sequence, and will instead insert the local variable $n (which is probably unset) as the default value:

/**
  * Don't use double quotes for defined values unless you really mean to.
  * If you want to use double quotes, make sure to escape the $n like: "\$n"
  */
Phactory::define('user', array('name' => "user$n")); //WRONG didn't escape $n in double quotes

$user0 = Phactory::create('user'); // name == 'user'
$user1 = Phactory::create('user'); // name == 'user'

Associations

Phactory provides support for defining the associations between tables. After you define an association from one table to another, you can create a new object in the first table and pass in an existing object from the second table. The appropriate columns or join tables will be automatically filled in the the correct values.

Documentation for the different types of associations Phactory supports is below. Any parameters marked null are optional; Phactory will attempt to guess the correct value based on a common naming convention if you omit them.

Phactory::manyToOne($to_table, $from_column = null, $to_column = null)

Phactory::define('author');

Phactory::define('book',
                 array('name' => 'Test Book'),
                 array('primary_author' => Phactory::manyToOne('author')));

$twain = Phactory::create('author', array('name' => 'Mark Twain'));

$book = Phactory::createWithAssociations('book', array('primary_author' => $twain));

$book->author_id == $twain->getId();

Note that in the above example ‘primary_author’ is the name of the association, and not necessarily the name of the table. You can name the association the same as your table if you wish.

Phactory::manyToMany($to_table, $join_table, $from_column = null, $from_join_column = null, $to_join_column = null, $to_column = null)

Phactory::define('tag');

Phactory::define('post',
                 array('name' => 'Test Blog Post'),
                 array('tag' => Phactory::manyToMany('tag', 'posts_tags')));

$tag = Phactory::create('tag', array('name' => 'PHP'));

$post = Phactory::createWithAssociations('post', array('tag' => $tag));

// a row in the join table 'posts_tags' has been created with $blog->getId() and $tag->getId()

Phactory::oneToOne() also exists, but it is just an alias of manyToOne.

Reset and Recall

Usually when you are unit testing, you’ll want your database to be reset after each test. Similarly, you might want your Phactory blueprints to be cleared out after each test or each test suite.

Phactory::recall() will empty any tables you defined blueprints for.

Phactory::reset() clears out all blueprints definitions, and calls Phactory::recall() to empty those tables in the database.

A Complete PHPUnit Example Test

/**
  * This is the function we will test.
  * It should retrieve a user from the db by id,
  * and return that user's age.
  *
  * @param PDO $pdo
  * @param int $user_id
  * @return mixed The age of the user, or false if no user
  */
function getUserAge($pdo, $user_id)
{
    $stmt = $pdo->prepare("SELECT * FROM `users` WHERE `id` = ?");
    $stmt->execute(array($user_id));
    $user = $stmt->fetch();

    if(false === $user) {
        return false;
    }

    return $user['age'];
}

require_once 'Phactory/lib/Phactory.php';
class UserTest extends PHPUnit_Framework_TestCase
{
    public static function setUpBeforeClass()
    {
        // create a db connection and tell Phactory to use it
        $pdo = new PDO("sqlite:test.db");
        Phactory::setConnection($pdo);

        /**
          * Normally you would not need to create a table here, and would use
          * an existing table in your test database instead.
          * For the sake of creating a self-contained example, we create
          * the 'users' table here.
          */
        $pdo->exec("CREATE TABLE `users` ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER )");

        // reset any existing blueprints and empty any tables Phactory has used
        Phactory::reset();

        // define default values for each user we will create
        Phactory::define('user', array('name' => 'Test User $n', 'age' => 18));
    }

    public static function tearDownAfterClass()
    {
        Phactory::reset();

        // since we created a table in this test, we must drop it as well
        Phactory::getConnection()->exec("DROP TABLE `users`");
    }

    public function testGetUserAge()
    {
        // test that getUserAge() returns false for a nonexistent user
        $age = getUserAge(Phactory::getConnection(), 0);
        $this->assertFalse($age);

        // create 20 users, with ages from 1-20
        $users = array();
        for($i = 1; $i <= 20; $i++) {
            // create a row in the db with age = $i, and store a Phactory_Row object
            $users[] = Phactory::create('user', array('age' => $i));
        }

        // test that getUserAge() returns the correct age for each user
        foreach($users as $user) {
            // Phactory_Row provides getId() which returns the value of the PK column
            $user_id = $user->getId();

            $age = getUserAge(Phactory::getConnection(), $user_id);

            $this->assertEquals($user->age, $age);
        }
    }
}