Adding the ManyToOne Relation

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Hmm. We want each Article to have many Comments... and we want each Comment to belong to one Article. Forget about Doctrine for a minute: let's think about how this should look in the database. Because each Comment should belong to one article, this means that the comment table needs an article_id column.

So far, in order to add a new column to a table, we add a new property to the corresponding entity. And, at first, adding a relationship column is no different: we need a new property on Comment.

Generating the Relationship

And, just like before, when you want to add a new field to your entity, the easiest way is to use the generator. Run:

php bin/console make:entity

Type Comment so we can add the new field to it. But then, wait! This is a very important moment: it asks us for the new property's name. If you think that this should be something like articleId... that makes sense. But, surprise! It's wrong!

Instead, use article. I'll explain why soon. For the field type, we can use a "fake" option here called: relation: that will start a special wizard that will guide us through the relation setup process.

The first question is:

What class should this entity be related to?

Easy: Article. Now, it explains the four different types of relationships that exist in Doctrine: ManyToOne, OneToMany, ManyToMany and OneToOne. If you're not sure which relationship you need, you can read through the descriptions to find the one that fits best.

Check out the ManyToOne description:

Each comment relates to one Article

That sound perfect! And then:

Each Article can have many Comment objects

Brilliant! This is the relationship we need. In fact, it's the "king" of relationships: you'll probably create more ManyToOne relationships than any other.

Answer with: ManyToOne.

Now, it asks us if the article property on Comment is allowed to be null. Basically, it's asking us if it should be legal for a Comment to be saved to the database that is not related to an Article, so, with an article_id set to null. A Comment must have an article, so let's say no.

Generating the Other (Inverse) Side of the Relation

This next question is really important: do we want to add a new property to Article? Here's the deal: you can look at every relationship from two different sides. You could look at a Comment and ask for its one related Article. Or, you could look at an Article, and ask for its many related comments.

No matter what we answer here, we will be able to get or set the Article for a Comment object. But, if we want, the generator can also map the other side of the relationship. This is optional, but it means that we will be able to say $article->getComments() to get all of the Comments for an Article. There's no real downside to doing this, except having extra code if you don't need this convenience. But, this sounds pretty useful. In fact, we can use it to render the comments on the article page!

If this is making your head spin, don't worry! We'll talk more about this later. But most of the time, because it makes life easier, you will want to generate both sides of a relationship. So let's say yes.

Then, for the name of this new property in Article, use the default: comments.

Finally, it asks you about something called orphanRemoval. Say no here. This topic is a bit more advanced, and you probably don't need orphanRemoval unless you're doing something complex with Symfony form collections. Oh, and we can easily update our code later to add this.

And... it's done! Hit enter one more time to exit. We did it!

Looking at the Entities

Because I committed all of my changes before recording, I'll run:

git status

to see what this did. Cool! It updated both Article and Comment. Open the Comment class first:

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

Awesome! It added a new property called article, but instead of the normal @ORM\Column, it used @ORM\ManyToOne, with some options that point to the Article class. Then, at the bottom, we have getter and setter methods like normal:

... lines 1 - 10
class Comment
{
... lines 13 - 66
public function getArticle(): ?Article
{
return $this->article;
}
public function setArticle(?Article $article): self
{
$this->article = $article;
return $this;
}
}

Now, check out the other side of the relationship, in Article entity. This has a new comments property:

... lines 1 - 13
class Article
{
... lines 16 - 60
/**
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article")
*/
private $comments;
... lines 65 - 200
}

And, near the bottom, three new methods: getComments(), addComment() and removeComment():

... lines 1 - 5
use Doctrine\Common\Collections\Collection;
... lines 7 - 13
class Article
{
... lines 16 - 170
/**
* @return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setArticle($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
// set the owning side to null (unless already changed)
if ($comment->getArticle() === $this) {
$comment->setArticle(null);
}
}
return $this;
}
}

You could also add a setComments() method: but addComment() and removeComment() are usually more convenient:

The ArrayCollection Object

Oh, and there's one little, annoying detail that I need to point out. Whenever you have a relationship that holds a collection of items - like how an Article will relate to a collection of comments, you need to add a __construct() method and initialize that property to a new ArrayCollection():

... lines 1 - 4
use Doctrine\Common\Collections\ArrayCollection;
... lines 6 - 13
class Article
{
... lines 16 - 65
public function __construct()
{
$this->comments = new ArrayCollection();
}
... lines 70 - 200
}

The generator took care of that for us. And, this looks scarier, or at least, more important than it really is. Even though the comments are set to an ArrayCollection object, I want you to think of that like a normal array. In fact, you can count, loop over, and pretty much treat the $comments property exactly like a normal array. The ArrayCollection is simply needed by Doctrine for internal reasons.

ManyToOne Versus OneToMany

Now, remember, we generated a ManyToOne relationship. We can see it inside Comment: the article property is a ManyToOne to Article. But, if you look at Article, huh. It has a OneToMany relationship back to Comment:

... lines 1 - 13
class Article
{
... lines 16 - 60
/**
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article")
*/
private $comments;
... lines 65 - 200
}

This is a really important thing. In reality, ManyToOne and OneToMany do not represent two different types of relationships! Nope, they describe the same, one relationship, just viewed from different sides.

Generating the Migration

Enough talking! Let's finally generate the migration. Find your terminal and run:

php bin/console make:migration

Go back to your editor and open that new migration file. Woh! Awesome! The end-result is super simple: it adds a new article_id column to the comment table along with a foreign key constraint to the article's id column:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180426185536 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE comment ADD article_id INT NOT NULL');
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C7294869C FOREIGN KEY (article_id) REFERENCES article (id)');
$this->addSql('CREATE INDEX IDX_9474526C7294869C ON comment (article_id)');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C7294869C');
$this->addSql('DROP INDEX IDX_9474526C7294869C ON comment');
$this->addSql('ALTER TABLE comment DROP article_id');
}
}

So even though, in Comment, we called the property article:

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

In the database, this creates an article_id column! Ultimately, the database looks exactly like we expected in the beginning! But in PHP, guess what? When we set this article property, we will set an entire Article object on it - not the Article's ID. More about that next.

The migration looks prefect. So find your terminal, and run it!

php bin/console doctrine:migrations:migrate

Ok, time to create a Comment object and learn how to relate it to an Article.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "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.2.7
        "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
    }
}