Foundry: Fixture Model Factories

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

In the load() method of the fixture class, we can create as much dummy data as we want. Right now... we're creating exactly one Question... which isn't making for a very realistic experience.

If we created more questions... and especially in the future when we will have multiple database tables that relate to each other, this class would start to get ugly. It's... already kind of ugly.

Hello Foundry!

No, we deserve better! Let's use a super fun new library instead. Google for "Zenstruck Foundry" and find its GitHub Page.

Foundry is all about creating Doctrine entity objects in an easy, repeatable way. It's perfect for fixtures as well as for functional tests where you want to seed your database with data at the start of each test. It even has extra features for test assertions!

The bundle was created by Kevin Bond - a long time Symfony contributor and friend of mine who's been creating some really excellent libraries lately. Foundry is Canadian for fun!

Installing Foundry

Let's get to work! Scroll down to the installation, copy the composer require line, find your terminal and paste. The --dev is here because we only need to load dummy data in the dev & test environments.

composer require zenstruck/foundry --dev

While that's running, head back to the docs. Let me show you what this bundle is all about. Suppose you have entities like Category or Post. The idea is that, for each entity, we will generate a corresponding model factory. So, a Post entity will have a PostFactory class, which will look something like this.

Once we have that, we can configure some default data for the entity class and then... start creating objects!

I know I explained that quickly, but that's because we're going to see this in action. Back at the terminal... let's wait for this to finish. I'm actually recording at my parents' house... where the Internet is barely a step up from dial-up.

After an edited break where I ate a sandwich and watched Moana, this finally finishes.

make:factory

Let's generate one of those fancy-looking model factories for Question. To do that, run:

symfony console make:factory

I also could have run bin/console make:factory... because this command doesn't need the database environment variables... but it's easier to get in the habit of always using symfony console.

Select Question from the list and... done! Go check out the new class src/Factory/QuestionFactory.php.

... lines 1 - 2
namespace App\Factory;
use App\Entity\Question;
use App\Repository\QuestionRepository;
use Zenstruck\Foundry\RepositoryProxy;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
/**
* @method static Question|Proxy findOrCreate(array $attributes)
* @method static Question|Proxy random()
* @method static Question[]|Proxy[] randomSet(int $number)
* @method static Question[]|Proxy[] randomRange(int $min, int $max)
* @method static QuestionRepository|RepositoryProxy repository()
* @method Question|Proxy create($attributes = [])
* @method Question[]|Proxy[] createMany(int $number, $attributes = [])
*/
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
// TODO add your default values here (https://github.com/zenstruck/foundry#model-factories)
];
}
protected function initialize(): self
{
// see https://github.com/zenstruck/foundry#initialization
return $this
// ->beforeInstantiate(function(Question $question) {})
;
}
protected static function getClass(): string
{
return Question::class;
}
}

Adding Default Values

The only method that we need to worry about right now is getDefaults(). The idea is that we'll return an array of all of the data needed to create a Question. For example, we can set a name key to our dummy question name - "Missing pants".

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => 'Missing pants',
... lines 26 - 40
];
}
... lines 43 - 55
}

This works a bit like Twig. When Foundry sees the name key, it will call the setName() method on Question. Internally, this uses Symfony's property-access component, which I'm mentioning, because it also supports passing data through the constructor if you need that.

Copy the rest of the dummy code from our fixture class, delete it... and delete everything actually.

... lines 1 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 14 - 15
$manager->flush();
}
}

Back in QuestionFactory, paste!

But we need to convert all of this into array keys. As exciting as this is... I'll... type really fast.

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => 'Missing pants',
'slug' => 'missing-pants-'.rand(0, 1000),
'question' => <<<EOF
Hi! So... I'm having a *weird* day. Yesterday, I cast a spell
to make my dishes wash themselves. But while I was casting it,
I slipped a little and I think `I also hit my pants with the spell`.
When I woke up this morning, I caught a quick glimpse of my pants
opening the front door and walking out! I've been out all afternoon
(with no pants mind you) searching for them.
Does anyone have a spell to call your pants back?
EOF
,
'askedAt' => rand(1, 10) > 2 ? new \DateTime(sprintf('-%d days', rand(1, 100))) : null,
'votes' => rand(-20, 50),
];
}
... lines 43 - 55
}

And.... done! Phew...

Using the Factory

Ok! We now have a simple array of "default" values that are enough to create a valid Question object. Our QuestionFactory is ready! Let's use it in AppFixtures.

How? First, say QuestionFactory::new(). That will give us a new instance of the QuestionFactory. Now ->create() to create a single Question.

... lines 1 - 5
use App\Factory\QuestionFactory;
... lines 7 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
QuestionFactory::new()->create();
$manager->flush();
}
}

Done! Ok, it's still not interesting - it will create just one Question... but let's try it! Re-run the fixtures:

symfony console doctrine:fixtures:load

Answer yes and... no errors! Head over to the browser, refresh and... oh! Zero questions! Ah, my one question is probably unpublished. Load the fixtures again:

symfony console doctrine:fixtures:load

Refresh and... there it is!

createMany()

At this point, you might be wondering: why is this better? Valid question. It's better because we've only just started to scratch the service of what Foundry can do. Want to create 20 questions instead of just one? Change create() to createMany(20).

... lines 1 - 5
use App\Factory\QuestionFactory;
... lines 7 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
QuestionFactory::new()->createMany(20);
$manager->flush();
}
}

That's it. Reload the fixtures again:

symfony console doctrine:fixtures:load

Then go check out the homepage. Hello 20 questions created with one line of very readable code.

But wait there's more Foundry goodness! Foundry comes with built-in support for a library called faker. A handy tool for creating truly fake data. Let's improve the quality of our fake data and see a few other cool things that Foundry can do next.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.9.0
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}