Owning Vs Inverse Sides of a Relation

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

There's a, kind of, complex topic in Doctrine relations that we need to talk about. It's the "owning versus inverse side" of a relationship.

We already know that any relation can be seen from two different sides: Question is a OneToMany to Answer...

... lines 1 - 14
class Question
{
... lines 17 - 51
/**
* @ORM\OneToMany(targetEntity=Answer::class, mappedBy="question")
*/
private $answers;
... lines 56 - 176
}

and that same relation can be seen as an Answer that is ManyToOne to Question.

... lines 1 - 11
class Answer
{
... lines 14 - 37
/**
* @ORM\ManyToOne(targetEntity=Question::class, inversedBy="answers")
* @ORM\JoinColumn(nullable=false)
*/
private $question;
... lines 43 - 95
}

So... what's the big deal? We already know that we can read data from both sides: we can say $answer->getQuestion() and we can also say $question->getAnswers().

Setting the Other Side of the Relation

But can you set data from both sides? In AnswerFactory, when we originally started playing with this relationship, we proved that you can say $answer->setQuestion() and Doctrine does correctly save that to the database.

Now let's try the other direction. I'm going to paste in some plain PHP code to play with. This uses the QuestionFactory to create one Question - I'm using it because I'm kinda lazy - and then creates two Answer objects by hand and persists them. We don't need to persist the Question because the QuestionFactory saves it entirely.

... lines 1 - 4
use App\Entity\Answer;
use App\Entity\Question;
... lines 7 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 29
$question = QuestionFactory::createOne();
$answer1 = new Answer();
$answer1->setContent('answer 1');
$answer1->setUsername('weaverryan');
$answer2 = new Answer();
$answer2->setContent('answer 1');
$answer2->setUsername('weaverryan');
$manager->persist($answer1);
$manager->persist($answer2);
... lines 40 - 41
}
}

At this point, the Question and these two answers are not related to each other. So, not surprisingly, if we run:

symfony console doctrine:fixtures:load

we get our favorite error: the question_id column cannot be null on the answer table. Cool! Let's relate them! But this time, instead of saying, $answer1->setQuestion(), do it with $question->addAnswer($answer1)... and $question->addAnswer($answer2).

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 29
$question = QuestionFactory::createOne();
$answer1 = new Answer();
... lines 32 - 33
$answer2 = new Answer();
... lines 35 - 36
$question->addAnswer($answer1);
$question->addAnswer($answer2);
... lines 41 - 44
}
}

If you think about it... this is really saying the same thing as when we set the relationship from the other direction: this Question has these two answers.

Let's see if it saves! Run the fixtures:

symfony console doctrine:fixtures:load

And... no errors! I think it worked! Double-check with:

SELECT * FROM answer

symfony console doctrine:query:sql 'SELECT * FROM answer'

Let's see... yea! Here are the new answers. Oh, apparently I called them both "answer 1" - silly Ryan. But more importantly, each answer is correctly related to a Question.

Ok! so it turns out you can set data from both sides. The two sides of the relationship apparently behave identically.

Now, at this point, you might be saying to yourself:

Why is this guy taking so much time to show me that something works exactly like I expect it too?

The "setters" Synchronize the Other Side of the Relation

Great question! Because... this doesn't really work like we just saw. Let me show you.

Open the Question class and find the addAnswer() method.

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

This was generated for us by the make:entity command. It first checks to see if the $answers property already contains this answer.... just to avoid a duplication. If it does not, it, of course, adds it to that property. But it also does something else, something very important: $answer->setQuestion($this). Yup, it sets the other side of the relation.

So if an Answer is added to a Question, that Question is also set onto that Answer. Now, watch what happens if we comment-out this line...

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
//$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

and then go reload the fixtures:

symfony console doctrine:fixtures:load

An error! The question_id column cannot be null on the answer table! It did not relate the Question to the Answer properly!

Owning vs Inverse

This is what I wanted to talk about. Each relation has two different sides and these sides have a name: the owning side and the inverse side. For a ManyToOne and OneToMany relationship, the owning side is always the ManyToOne side. And it's easy to remember: the owning side is where the foreign key column lives in the database. In this case, the answer table will have a question_id column so this is the "owning" side.

The OneToMany side is called the inverse side.

Why is this important? It's important because, when Doctrine saves an entity, it only looks at the data on the owning side of a relationship. Yup, it looks at the $question property on the Answer entity to figure out what to save to the database. It completely ignores the data on the inverse side. Really, the inverse side exists solely for the convenience of us reading that data: the convenience of being able to say $question->getAnswers().

So right now, we are only setting the inverse side of the relationship. And so, when it saves the Answer, it does not link the Answer to this Question.

Inverse Side is Optional

And actually, the inverse side of a relationship is entirely optional. The make:entity command asked us if we wanted to map this side of the relationship. We could delete everything inside of Question that's related to answers, and the relationship would still be set up in the database and we could still use it. We just wouldn't be able to say $question->getAnswers().

I'm telling you all this so that you can avoid potential WTF moments if you relate two objects... but they mysteriously don't save. Fortunately, the make:entity command takes care of all this ugliness for us by generating really smart addAnswer() and removeAnswer() methods that synchronize the owning side of the relationship. So unless you don't use make:entity or start deleting code, you won't need to think about this problem on a day-to-day basis.

Put back the $answer->setQuestion() code so that we can, once again, safely set the data from either side.

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

Back in the fixtures, now that we've learned all of this, delete the custom code.

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
$questions = QuestionFactory::createMany(20);
QuestionFactory::new()
->unpublished()
->many(5)
->create()
;
AnswerFactory::createMany(100, function() use ($questions) {
return [
'question' => $questions[array_rand($questions)]
];
});
$manager->flush();
}
}

And then, let's reload our fixtures:

symfony console doctrine:fixtures:load

Next: when we call $question->getAnswers()... which we're currently doing inside of our template, what order is it returning those answers? And can we control that order? Plus we'll learn a config trick to optimize the query that's made when all we need to do is count the number of items in a relationship.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.1.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.15.1
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1
    }
}