Buy
Buy

Owning Vs Inverse Relations

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

Login Subscribe

We 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.

Set the Inverse Side

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!

Synchronizing the Owning Side

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!

Owning Versus Inverse

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.

The Inverse Side is Optional

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.

Leave a comment!

  • 2018-09-21 Victor Bocharsky

    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!

  • 2018-09-20 Student

    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.