Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Relations in Foundry

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We're using a library called Foundry to help us generate rich fixtures data. Right now, it's creating 25 questions. Let's use Foundry to also add some answers.

make:factory Answer

Start by generating the factory class. At your terminal, run:

symfony console make:factory

Yup: we want to generate a factory for the Answer entity. Beautiful! Let's go check that out: src/Factory/AnswerFactory.php.

... lines 1 - 2
namespace App\Factory;
use App\Entity\Answer;
use App\Repository\AnswerRepository;
use Zenstruck\Foundry\RepositoryProxy;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
/**
* @extends ModelFactory<Answer>
*
... lines 14 - 27
*/
final class AnswerFactory extends ModelFactory
{
public function __construct()
{
parent::__construct();
// TODO inject services if required (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services)
}
protected function getDefaults(): array
{
return [
// TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
'content' => self::faker()->text(),
'username' => self::faker()->text(),
'createdAt' => null, // TODO add DATETIME ORM type manually
'updatedAt' => null, // TODO add DATETIME ORM type manually
];
}
protected function initialize(): self
{
// see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
return $this
// ->afterInstantiate(function(Answer $answer) {})
;
}
protected static function getClass(): string
{
return Answer::class;
}
}

Cool. The only work we need to do immediately is inside getDefaults(). The goal here is to give every required property a default value... and we even have Faker available here to help us generate some random stuff.

Let's see: for username, we can use a userName() faker method. And for votes, instead of a random number, use numberBetween -20 and 50. I'll delete updatedAt... but keep createdAt so we can fake answers with a dateTimeBetween() -1 year and now, which is the default 2nd argument. That period is a typo for future me to discover!

... lines 1 - 28
final class AnswerFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
'content' => self::faker()->text(),
'username' => self::faker()->userName(),
'createdAt' => self::faker()->dateTimeBetween('-1 year'),
'votes' => rand(-20, 50),
];
}
... lines 47 - 59
}

Head back to AppFixtures. Let's remove all of this manual Answer and Question code. Replace it with AnswerFactory::createMany(100) to create 100 answers.

... lines 1 - 6
use App\Factory\AnswerFactory;
... lines 8 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
QuestionFactory::createMany(20);
QuestionFactory::new()
->unpublished()
->many(5)
->create()
;
AnswerFactory::createMany(100);
$manager->flush();
}
}

Populating the Answer.question Property

Over in AnswerFactory... let's fix that typo. Notice that, in getDefaults(), we are not setting the question property. And so, if you spin over to your terminal and run:

symfony console doctrine:fixtures:load

... we get our favorite error: question_id column cannot be null.

To fix this, in AppFixtures, pass a 2nd argument to createMany(): an array with a question key set to QuestionFactory::random(), which is a really cool method.

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 23
AnswerFactory::createMany(100, [
'question' => QuestionFactory::random(),
]);
... lines 27 - 28
}
}

With this setup, when we call createMany(), Foundry will first call getDefaults(), grab that array, add question to it, and then will ultimately try to create the Answer using all of those values.

The QuestionFactory::random() method does what it sounds like: it grabs a random Question from the database. So yes, it is now important that we create the questions first and then the answers after.

Let's try this:

symfony console doctrine:fixtures:load

Ok... no errors. Check out the database:

symfony console doctrine:query:sql 'SELECT * FROM answer'

Passing a Callback to Randomize Every Answer's Data

And... sweet! We have 100 answers filled with a lot of nice random data from Faker. But... if you look closely, we have a teensy problem. This answer has question_id 140... and so does this one... and this one! In fact, all 100 answers are related to the same Question. Whoops!

Why? Because the QuestionFactory::random() method is called just once. It did fetch a random Question... and then used that same random question for all 100 answers.

If you want a different value per Answer, you need to pass a callback function to the second argument instead of an array. That function will then return the array of data to use. Foundry will execute the callback once for each Answer: so 100 times in total.

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 23
AnswerFactory::createMany(100, function() {
return [
'question' => QuestionFactory::random(),
];
});
... lines 29 - 30
}
}

Try it again: reload the fixtures:

symfony console doctrine:fixtures:load

Then query the answer table:

symfony console doctrine:query:sql 'SELECT * FROM answer'

Much better! 100 answers where each is related to a random question.

Moving the "question" into getDefaults()

But to make life easier, we can move this question value directly into AnswerFactory. Copy the question line.. and then change the fixtures code back to the very simple AnswerFactory::createMany(100).

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 23
AnswerFactory::createMany(100, function() {
return [
'question' => QuestionFactory::random(),
];
});
... lines 29 - 30
}
}

Now in AnswerFactory, paste question set to QuestionFactory::random(). This works because the getDefaults() method is called 100 times, once for each answer.

... lines 1 - 28
final class AnswerFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
... lines 41 - 44
'question' => QuestionFactory::random(),
];
}
... lines 48 - 60
}

Next: let's discover a key rule when using Foundry and relationships. A rule that, if you forget to follow it, might result in a bunch of random extra records in your database.

Leave a comment!

1
Login or Register to join the conversation
t5810 Avatar

Hi

In case that anyone else is on windows as I am, and is coding along, when running the query:
symfony console doctrine:query:sql 'SELECT * FROM answer' use double quotes, instead of the single quotes, or you will get an error:

*symfony console doctrine:query:sql 'SELECT FROM answer'**

Cheers

2 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/validator": "5.3.*", // v5.3.14
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1
    }
}
userVoice