This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
05.

Setting Relations in Foundry

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

OK, 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):

45 lines | src/DataFixtures/AppFixtures.php
// ... 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 in starship_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:

71 lines | src/Factory/StarshipPartFactory.php
// ... 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:

71 lines | src/Factory/StarshipPartFactory.php
// ... 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():

47 lines | src/DataFixtures/AppFixtures.php
// ... 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:

47 lines | src/DataFixtures/AppFixtures.php
// ... 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():

71 lines | src/Factory/StarshipPartFactory.php
// ... 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():

71 lines | src/Factory/StarshipPartFactory.php
// ... 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:

45 lines | src/DataFixtures/AppFixtures.php
// ... lines 1 - 12
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
// ... lines 17 - 41
StarshipPartFactory::createMany(100);
}
}

And... switch back to randomOrCreate()

71 lines | src/Factory/StarshipPartFactory.php
// ... 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.