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 SubscribeThere'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()
.
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?
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!
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
.
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.
Hey @Yuki-K
woh that's complex question, by default if you didn't configured to store data on your local drive iit stores it inside the container, and it keeps data saved until you didn't destroyed it. But if you want a persisted data you should map you local filesystem with container filesystem so in this case you will not lose any data even if container will be dropped.
Cheers!
The question asks us to pick the answer that is incorrect, but they're all correct.
Hey Peter,
Yes, this is a tricky question, and that's why the right option is the one that says [SPOILER ALERT] "All of these statements are true"
Cheers!
But the question is for what is is not true. So by selecting that answer you're saying it's not true that all of the statements are true, i.e. they're all false.
Ohh I think I got your point. Do you think if we reword the option to say something like "None, all of these statements are correct" would make more sense?
Hi, just to point out that there is no more subtitle from this video, then re-appear on the 11th: "Collection Criteria for Custom Relation Queries"
Thanks for pointing that out - we'll get it fixed up - we had a hiccup with our subtitling system and we need to reprocess a couple of them!
// composer.json
{
"require": {
"php": ">=8.1",
"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.2.1
"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.17.5
"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/validator": "5.3.*", // v5.3.14
"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
}
}
Hi
I was wondering why, when I close the terminal (stop the mysql container inside docker) and hit
docker compose up
again, the datas remain in the project.I thought database holds the datas, but where the datas go when I close my terminal and stop the mysql container?
Is it because stop the container doesn't mean delete and databases and recreate them, but instead
changing the port number to the same database?
docker makes me headache, Help me! lol
Cheers!