Relations in Foundry
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.
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