This course is still being released! Check back later for more chapters.
Setting Relations in Foundry
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOK, we have a couple of parts and a few starships, but to fill our testing data fleet: I want a lot more. This is a job perfectly suited for our good friend: Foundry. Remove the manual code, then anywhere, say: StarshipPartFactory::createMany(100)
:
// ... lines 1 - 8 | |
use App\Factory\StarshipPartFactory; | |
// ... lines 10 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 17 - 41 | |
StarshipPartFactory::createMany(100); | |
} | |
} |
And try the fixtures:
symfony console doctrine:fixtures:load
Uh-oh!
starship_id
cannot be null instarship_part
.
This traces all the way back to StarshipPartFactory
, down in the defaults()
method. This is the data passed to each new StarshipPart
when it's created. The golden rule is to make defaults()
return a key for every required property on the object. Right now, we're obviously missing the starship
property, so let's add that. Set starship
, not starship_id
, to a nifty method called Starship::randomOrCreate()
and pass an array:
// ... lines 1 - 11 | |
final class StarshipPartFactory extends PersistentProxyObjectFactory | |
{ | |
// ... lines 14 - 45 | |
protected function defaults(): array|callable | |
{ | |
// ... lines 48 - 50 | |
return [ | |
// ... lines 52 - 54 | |
'starship' => StarshipFactory::randomOrCreate([ | |
// ... line 56 | |
]), | |
]; | |
} | |
// ... lines 60 - 69 | |
} |
Setting the Stage for Starship Parts
On the homepage, we're only listing starships with 'in progress' or 'waiting' status. To make sure these parts are related to a ship with 'in progress' status, add a status
key in the array set to StarshipStatusEnum::IN_PROGRESS
:
// ... lines 1 - 5 | |
use App\Entity\StarshipStatusEnum; | |
// ... lines 7 - 11 | |
final class StarshipPartFactory extends PersistentProxyObjectFactory | |
{ | |
// ... lines 14 - 45 | |
protected function defaults(): array|callable | |
{ | |
// ... lines 48 - 50 | |
return [ | |
// ... lines 52 - 54 | |
'starship' => StarshipFactory::randomOrCreate([ | |
'status' => StarshipStatusEnum::IN_PROGRESS, | |
]), | |
]; | |
} | |
// ... lines 60 - 69 | |
} |
This randomOrCreate()
is an impressive method: it first looks in the database to find a Starship
that matches these criteria (an "in progress" ship"). If it finds one, it uses that. If it does not, it creates one with that status.
Try the fixtures now.
symfony console doctrine:fixtures:load
No errors! Check the database:
symfony console doctrine:query:sql "SELECT * FROM starship_part"
Look closely... Ok! we have 100 parts each tied to a random Starship
, which should be a Starship
with an 'in progress' status. I think that was my most productive 5 minutes ever!
Taking Control in Foundry
But what if we need more control? What if we want to assign all 100 of these parts to the same ship? I know it sounds a bit not useful, but it'll help us understand Foundry and relationships even better.
Start by getting a ship variable: $ship = StarshipFactory::createOne()
:
// ... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 17 - 32 | |
$ship = StarshipFactory::createOne([ | |
// ... lines 34 - 38 | |
]); | |
// ... lines 40 - 44 | |
} | |
} |
Then, in StarshipPartFactory::createMany()
, pass a second argument to specify that we want starship
to be set to this specific ship:
// ... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 17 - 32 | |
$ship = StarshipFactory::createOne([ | |
// ... lines 34 - 38 | |
]); | |
// ... lines 40 - 41 | |
StarshipPartFactory::createMany(100, [ | |
'starship' => $ship, | |
]); | |
} | |
} |
Load up those fixtures again.
symfony console doctrine:fixtures:load
And done! All parts are now related to the same one ship. And if we query the Starship
, we have 23: the 20 at the bottom, plus the extra 3 we added. Everything's coming together!
The Foundry Plot Twist
Here's where things get interesting. In StarshipPartFactory
, instead of using randomOrCreate()
, switch to createOne()
:
// ... lines 1 - 11 | |
final class StarshipPartFactory extends PersistentProxyObjectFactory | |
{ | |
// ... lines 14 - 45 | |
protected function defaults(): array|callable | |
{ | |
// ... lines 48 - 50 | |
return [ | |
// ... lines 52 - 54 | |
'starship' => StarshipFactory::createOne([ | |
// ... line 56 | |
]), | |
]; | |
} | |
// ... lines 60 - 69 | |
} |
Load the fixtures again.
symfony console doctrine:fixtures:load
And... query for all the ships.
symfony console doctrine:query:sql "SELECT * FROM starship"
Whoa, we suddenly have a fleet! 123 ships to be exact. What happened?
For each part, defaults()
is called. So for all 100 parts, it's triggering this line, which creates and saves a Starship
, even though we never use that Starship
because we override it moments later.
The solution? Change this to StarshipFactory::new()
:
// ... lines 1 - 11 | |
final class StarshipPartFactory extends PersistentProxyObjectFactory | |
{ | |
// ... lines 14 - 45 | |
protected function defaults(): array|callable | |
{ | |
// ... lines 48 - 50 | |
return [ | |
// ... lines 52 - 54 | |
'starship' => StarshipFactory::new([ | |
// ... line 56 | |
]), | |
]; | |
} | |
// ... lines 60 - 69 | |
} |
This is the secret sauce: it creates a new instance of the factory, not an object in the database. Try it:
symfony console doctrine:fixtures:load
Query the ships.
symfony console doctrine:query:sql "SELECT * FROM starship"
Perfect! We're back to 23.
Factories are Object Recipes
Fun fact! We can use these factory instances like recipes for creating objects. StarshipFactory::new(['status' => StarshipStatusEnum::STATUS_IN_PROGRESS])
does not create an object in the database. Nope: new()
means a new instance of the factory. And when you pass a factory for a property, Foundry delays creating that object until and if it's needed. So, only if the Starship
is not overridden will it create a new Starship
with status "in progress" and save it. This is actually the best-practice when setting relationships in Foundry: set them to a factory instance.
Clean up our fixtures by removing the override:
// ... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 17 - 41 | |
StarshipPartFactory::createMany(100); | |
} | |
} |
And... switch back to randomOrCreate()
// ... lines 1 - 11 | |
final class StarshipPartFactory extends PersistentProxyObjectFactory | |
{ | |
// ... lines 14 - 45 | |
protected function defaults(): array|callable | |
{ | |
// ... lines 48 - 54 | |
'starship' => StarshipFactory::randomOrCreate([ | |
// ... line 56 | |
]), | |
]; | |
} | |
// ... lines 60 - 69 | |
} |
Because, let's be honest, it's a pretty useful method.
Reload the fixtures one last time to make sure we didn't break anything
symfony console doctrine:fixtures:load
Nope! We'll try harder next time.
there's no code snippet in this tutorial visible, but you're saying:
"Set starship, not starship_id, to a nifty method called Starship::randomOrCreate() and pass an array."
then
symfony console doctrine:fixtures:load
works OK