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 SubscribeWe need to talk about a very, very important and, honestly, super-confusing part of Doctrine relations! Listen closely: this is the ugliest thing we need to talk about. So, let's get through it, put a big beautiful check mark next to it, then move on!
It's called the owning versus inverse sides of a relationship, and it's deals with the fact that you can always look at a single relationship from two different directions. You can either look at the Comment
and say that this has one article
, so, this is ManyToOne
to Article
:
... lines 1 - 7 | |
/** | |
* @ORM\Entity(repositoryClass="App\Repository\CommentRepository") | |
*/ | |
class Comment | |
{ | |
... lines 13 - 31 | |
/** | |
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $article; | |
... lines 37 - 77 | |
} |
Or you can go to Article
and - for that same one relationship, you can say that this Article
has many comments:
... lines 1 - 10 | |
/** | |
* @ORM\Entity(repositoryClass="App\Repository\ArticleRepository") | |
*/ | |
class Article | |
{ | |
... lines 16 - 60 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article") | |
*/ | |
private $comments; | |
... lines 65 - 200 | |
} |
So, what's the big deal then? We already know that you can read data from either direction. You can say $comment->getArticle()
or $article->getComments()
. But, can you also set data on both sides? Well... that's where things get interesting.
In ArticleFixtures
, we've proven that you can use $comment->setArticle()
to set the relationship:
... lines 1 - 8 | |
class ArticleFixtures extends BaseFixture | |
{ | |
... lines 11 - 27 | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) { | |
... lines 31 - 64 | |
$comment1->setArticle($article); | |
... lines 66 - 70 | |
$comment2->setArticle($article); | |
... line 72 | |
}); | |
... lines 74 - 75 | |
} | |
} |
Everything persists perfectly to the database. But now, comment those out. Instead, set the data from the other direction: $article->addComment($comment1)
and $article->addComment($comment2)
:
... lines 1 - 8 | |
class ArticleFixtures extends BaseFixture | |
{ | |
... lines 11 - 27 | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) { | |
... lines 31 - 64 | |
//$comment1->setArticle($article); | |
... lines 66 - 70 | |
//$comment2->setArticle($article); | |
... lines 72 - 73 | |
$article->addComment($comment1); | |
$article->addComment($comment2); | |
}); | |
... lines 77 - 78 | |
} | |
} |
We're adding the comments to the comments
collection on Article
. By the way, don't worry that this code lives after the call to persist()
. That's actually fine: the code just needs to come before flush()
.
Anyways, let's try it! Find your terminal, sip some coffee, and reload the fixtures:
php bin/console doctrine:fixtures:load
Ok, no errors! Check the database:
php bin/console doctrine:query:sql 'SELECT * FROM comment'
Yea! It does still work! The comments saved correctly, and each has its article_id
set.
So, I guess we found our answer: you can get and set data from either side of the relationship. Well... that's not actually true!
Hold Command
or Ctrl
and click the addComment()
method to jump into it:
... lines 1 - 13 | |
class Article | |
{ | |
... lines 16 - 178 | |
public function addComment(Comment $comment): self | |
{ | |
if (!$this->comments->contains($comment)) { | |
$this->comments[] = $comment; | |
$comment->setArticle($this); | |
} | |
return $this; | |
} | |
... lines 188 - 200 | |
} |
Look closely: this code was generated by the make:entity
command. First, it checks to make sure the comment isn't already in the comments
collection, just to avoid duplication on that property. Then, of course, it adds the comment to the $comments
property. But then, it does something very important: it calls $comment->setArticle($this)
.
Yep, this code synchronizes the data to the other side of the relationship. It makes sure that if you add this Comment
to this Article
, then the Article
is also set on the Comment
.
Let's try something: comment out the setArticle()
call for a moment:
... lines 1 - 13 | |
class Article | |
{ | |
... lines 16 - 178 | |
public function addComment(Comment $comment): self | |
{ | |
if (!$this->comments->contains($comment)) { | |
$this->comments[] = $comment; | |
//$comment->setArticle($this); | |
} | |
return $this; | |
} | |
... lines 188 - 200 | |
} |
Then, go back to your terminal and reload the fixtures:
php bin/console doctrine:fixtures:load
Woh! Explosion! When Doctrine tries to save the first comment, its article_id
is empty! The relationship is not being set correctly!
This is exactly what I wanted to talk about. Every relationship has two sides. One side is known as the owning side of the relation and the other is known as the inverse side of the relation. For a ManyToOne and OneToMany relation, the owning side is always the ManyToOne side:
... lines 1 - 10 | |
class Comment | |
{ | |
... lines 13 - 31 | |
/** | |
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $article; | |
... lines 37 - 77 | |
} |
And, it's easy to remember: the owning side is the side where the actual column appears in the database. Because the comment
table will have the article_id
column, the Comment.article
property is the owning side. And so, Article.comments
is the inverse side:
... lines 1 - 13 | |
class Article | |
{ | |
... lines 16 - 60 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article") | |
*/ | |
private $comments; | |
... lines 65 - 200 | |
} |
The reason this is so important is that, when you relate two entities together and save, Doctrine only looks at the owning side of the relationship to figure out what to persist to the database. Right now, we're only setting the inverse side! When Doctrine saves, it looks at the article
property on Comment - the owning side - sees that it is null, and tries to save the Comment with no Article!
The owning side is the only side where the data matters when saving. In fact, the entire purpose of the inverse side of the relationship is just... convenience! It only exists because it's useful to be able to say $article->getComments()
. That was particularly handy in the template.
Heck, the inverse side of a relationship is even optional! The make:entity
command asked us if we wanted to generate the inverse side. We could delete all of the comments
stuff from Article
, and the relationship would still exist in the database exactly like it does now. And, we could still use it. We wouldn't have our fancy $article->getComments()
shortcut anymore, but everything else would be fine.
I'm explaining this so that you can hopefully avoid a huge WTF moment in the future. If you ever try to relate two entities together and it is not saving, it may be due to this problem.
I'm going to uncomment the setArticle()
call:
... lines 1 - 13 | |
class Article | |
{ | |
... lines 16 - 178 | |
public function addComment(Comment $comment): self | |
{ | |
if (!$this->comments->contains($comment)) { | |
$this->comments[] = $comment; | |
$comment->setArticle($this); | |
} | |
return $this; | |
} | |
... lines 188 - 200 | |
} |
In practice, when you use the make:entity
generator, it takes care of this ugliness automatically, by generating code that synchronizes the owning side of the relationship when you set the inverse side. But, keep this concept in mind: it may eventually bite you!
Back in ArticleFixtures
, refactor things back to $comment->setArticle()
:
... lines 1 - 8 | |
class ArticleFixtures extends BaseFixture | |
{ | |
... lines 11 - 27 | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) { | |
... lines 31 - 64 | |
$comment1->setArticle($article); | |
... lines 66 - 70 | |
$comment2->setArticle($article); | |
... line 72 | |
}); | |
... lines 74 - 75 | |
} | |
} |
But, we know that, thanks to the code that was generated by make:entity
, we could instead set the inverse side.
Next, let's setup our fixtures properly, and do some cool stuff to generate comments and articles that are randomly related to each other.
hey Mike I.
Woohoo! You have a good eyes! Thanks for your attention! We fixed it, soon it will be online =)
Cheers
Nice course, one question, how I can do parent/child comments? Exp. User want to reply post.
Update comment interesting too. For give user edit it.
Thanks.
Hey Student,
For parent/children comments you need to add more mapping to Comment action, i.e. add more fields. So exactly as you said, you need to add Comment::$parent field to store parent comments and its relation will be Comment ManyToOne to Comment. And add Comment::$children so it's relation will be Comment OneToMany to Comment. So you need to self-referencing mapping.
About editing: you just need to create a Comment edit form and fill it in with data of the comment you want to edit. Of course, there should be some security check to prevent editing not yours comment :) We do not talk about forms in this course, see how to create forms: https://symfonycasts.com/sc... - but this course if on the planning stage yet. Or check out the same course but from the Symfony 3 track: https://symfonycasts.com/sc...
I hope this helps.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.7.2
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.4
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.0.4
"symfony/console": "^4.0", // v4.0.14
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.0.14
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/twig-bundle": "^4.0", // v4.0.4
"symfony/web-server-bundle": "^4.0", // v4.0.4
"symfony/yaml": "^4.0", // v4.0.14
"twig/extensions": "^1.5" // v1.5.1
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"fzaninotto/faker": "^1.7", // v1.7.1
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
"symfony/dotenv": "^4.0", // v4.0.14
"symfony/maker-bundle": "^1.0", // v1.4.0
"symfony/monolog-bundle": "^3.0", // v3.1.2
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.0.4
}
}
Ola!
In line php bin/console doctrine:query:sql 'SELECT * FROM comment"
You have a liltle mistake
You forgot a ' instead " in the end of line
Cheers