This course is still being released! Check back later for more chapters.
The Two Sides of a Relation: Owning vs Inverse
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 SubscribeFun fact for your upcoming Doctrine taco party: Every relationship can be viewed from two different sides. Take Starship
: it has multiple parts, making it a one-to-many relationship from the Starship
perspective. But, flip the telescope around and look from the StarshipPart
end, and you'll find a many-to-one relationship. One of these perspectives is always known as the owning side, and the other, the inverse side.
Now, you might be thinking:
Why do I care how the sides are named? I need to go feed my cat!
Tell Mittens to chill for three minutes: this might just save you from a big headache down the road... and a completely missed meal.
The Owning Side Unveiled
First off, which side is the owning side? For a many-to-one: it's always the side that has the ManyToOne
attribute, which is on the entity that will have the foreign key column. In our case, that's StarshipPart
.
The Importance of Ownership
But why does this matter? Two reasons. First, the JoinColumn
can only live on the owning side. And that makes sense: it controls the foreign key column. Second, you can only set the owning side of the relationship. Let me show you:
Pop open src/DataFixtures/AppFixtures.php
and let's play a bit: $starship = StarshipFactory::createOne();
. My AI overlord was almost right. Below this, I'll sprinkle in code that creates two StarshipPart
objects, persists & flushes them:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
$starship = StarshipFactory::createOne(); | |
$part1 = new StarshipPart(); | |
$part1->setName('Warp Core'); | |
$part1->setPrice(1000); | |
$part2 = new StarshipPart(); | |
$part2->setName('Phaser Array'); | |
$part2->setPrice(500); | |
$manager->persist($part1); | |
$manager->persist($part2); | |
// ... lines 25 - 52 | |
} | |
} |
I haven't set any relations yet, but let's recklessly load the fixtures anyway:
symfony console doctrine:fixtures:load
Our favorite error pops up
starship_id
cannot be null
Totally expected.
The Owning vs Inverse Side in Action
To demonstrate the owning vs inverse issue, add _real()
to the end of $starship
:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
$starship = StarshipFactory::createOne()->_real(); | |
// ... lines 17 - 52 | |
} | |
} |
When you create an entity via foundry, it actually wraps it in a little gift called a proxy object. This usually doesn't matter, but occasionally, it can cause some confusion. By calling _real()
, we unwrap the proxy and get the real Starship
object.
Time to connect these parts to this starship. Normally, we'd say $part1->setStarship($starship);
, which sets the owning side. This time try setting the inverse side. That would be $starship->addPart($part1);
and $starship->addPart($part2);
:
// ... lines 1 - 11 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
$starship = StarshipFactory::createOne()->_real(); | |
// ... lines 17 - 22 | |
$manager->persist($part1); | |
$manager->persist($part2); | |
$starship->addPart($part1); | |
$starship->addPart($part2); | |
// ... lines 28 - 55 | |
} | |
} |
Based on what I just explained, this should not work because we are only setting the inverse side. But let's roll the dice and load the fixtures anyway:
symfony console doctrine:fixtures:load
But surprise, surprise! No errors. In fact, if you check the database:
symfony console doctrine:query:sql "SELECT * FROM starship_part"
Sure enough, we have two new parts each related to a starship.
So, what gives? We just set the inverse side of the relationship, and it still saved to the database. That's the opposite of what I just told you!
The Plot Twist: Inverse Side Setting the Owning Side
Open the Starship
entity and find the addPart()
method:
// ... lines 1 - 13 | |
class Starship | |
{ | |
// ... lines 16 - 159 | |
public function addPart(StarshipPart $part): static | |
{ | |
if (!$this->parts->contains($part)) { | |
$this->parts->add($part); | |
$part->setStarship($this); | |
} | |
return $this; | |
} | |
// ... lines 169 - 180 | |
} |
Aha! This method calls $part->setStarship($this);
. It sets the owning side. When we set the inverse side, our own code generated by the make:entity
command also sets the owning side. Clever girl, eh?
## Owning vs Inverse vs I don't Care
So here's the takeaway: every relation has an owning side and an inverse side. The inverse side is optional. make:entity
asked if we wanted to generate the inverse side, and we said yes. That gave us the super convenient $ship->getParts()
method.
So yes, technically, you can only set the relationship from the owning side (i.e., $starshipPart->setShip()
), but in practice, you can set it from either side thanks to our own code that synchronizes both sides. So go astound your friends with your newfound knowledge, then promptly forget about it: it's not critical in practice.
Clean up our temporary code here and freshen things up by reloading the fixtures:
symfony console doctrine:fixtures:load
Alright, next up: orphanRemoval
. It's not nearly as mean as it sounds.