Setting Many To Many Relations
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 SubscribeAlright, let's dive into the final part of ManyToMany
. In one corner, we have the Starship
entity, which is linked via a ManyToMany
relationship with the Droid
entity. This relationship gives us an extra table called a "join table" keeping track of which droids have hitched a ride on which starships. But how do we assign a Droid
to a Starship
? Jump into AppFixtures
.
Adding some droids
First, let's toss a few droids into the mix. I'll add code that constructs three droids. Import the class with a quick option or Alt
+ Enter
:
// ... lines 1 - 4 | |
use App\Entity\Droid; | |
// ... lines 6 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 16 - 22 | |
$droid1 = new Droid(); | |
$droid1->setName('IHOP-123'); | |
$droid1->setPrimaryFunction('Pancake chef'); | |
$manager->persist($droid1); | |
$droid2 = new Droid(); | |
$droid2->setName('D-3P0'); | |
$droid2->setPrimaryFunction('C-3PO\'s voice coach'); | |
$manager->persist($droid2); | |
$droid3 = new Droid(); | |
$droid3->setName('BONK-5000'); | |
$droid3->setPrimaryFunction('Comedy sidekick'); | |
$manager->persist($droid3); | |
$manager->flush(); | |
// ... lines 38 - 63 | |
} | |
} |
And... we have droids! Nothing fancy though: creating a new Droid
, setting the required properties, persisting and flushing.
Assigning Droids to Starships
Now, let's get to the fun part: assigning a Droid
to a Starship
. Create a Starship
variable and get ready for the magic:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
$starship = StarshipFactory::createOne([ | |
// ... lines 17 - 21 | |
]); | |
// ... lines 23 - 63 | |
} | |
} |
The way to relate these two entities is surprisingly simple, and it's going to feel like a déjà vu from our OneToMany
relationship. I bet you can even guess!
Before the flush()
it's: $starship->addDroid($droid1)
. Do the same for the other two droids — $starship->addDroid($droid2)
and $starship->addDroid($droid3)
:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
$starship = StarshipFactory::createOne([ | |
// ... lines 17 - 21 | |
]); | |
// ... lines 23 - 25 | |
$starship->addDroid($droid1); | |
$manager->persist($droid1); | |
// ... lines 28 - 31 | |
$starship->addDroid($droid2); | |
$manager->persist($droid2); | |
// ... lines 34 - 37 | |
$starship->addDroid($droid3); | |
$manager->persist($droid3); | |
$manager->flush(); | |
// ... lines 41 - 66 | |
} | |
} |
The crew is ready for their droid-made pancakes, so let's try this!
symfony console doctrine:fixtures:load
No errors. To see if it's really working, run:
symfony console doctrine:query:sql 'SELECT * FROM droid'
As expected: three rows, one for each droid we created. Now, peek at that join table, starship_droid
.
symfony console doctrine:query:sql 'SELECT * FROM starship_droid'
Woot! Three rows, one for each droid to ship assignment.
The Magic of Doctrine
The real magic is that with Doctrine, all we need to worry about is relating a Droid
object to a Starship
object. Then, it takes care of the rest, handling the inserting and deleting of rows in the join table.
After the flush, we know we have three rows in the join table. Now, after the flush, remove an assignment: $starship->removeDroid($droid1)
:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 16 - 39 | |
$manager->flush(); | |
$starship->removeDroid($droid1); | |
$manager->flush(); | |
// ... lines 44 - 69 | |
} | |
} |
Reload the fixtures and check out the join table.
symfony console doctrine:query:sql 'SELECT * FROM droid'
Only two rows remain! Doctrine removed the row for our removed droid.
Owning vs Inverse Sides
One final touch on ManyToMany
— remember when we discussed owning versus inverse sides of a relationship? As we saw, our methods synchronize the other side of the relationship, adding the Droid
to the Starship
when we call addDroid()
:
// ... lines 1 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 207 | |
public function addDroid(Droid $droid): static | |
{ | |
if (!$this->droids->contains($droid)) { | |
$this->droids->add($droid); | |
} | |
return $this; | |
} | |
// ... lines 216 - 222 | |
} |
So the owning side doesn't matter much.
But which side is the owning side? In a ManyToMany
, either side could be the owning side.
To figure out who's the boss, look at the inversedBy
option. It says ManyToMany
and inversedBy: starships
, which means that the Droid.starships
property is the inverse side.
Now, this is mostly trivial, but if you're a control freak and want to dictate the name of the join table, you can add a JoinTable
attribute. But remember, it has to go on the owning side. Other than that, don't sweat it: no big deal.
Next, let's use the new relationship to render the droids assigned to each ship.
I had a problem with starship_droid table, after loading fixtures there is only one row - the last droid.
I add
_real()
and it works correctly