Buy Access to Course
17.

ManyToMany Relationship

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

We need the ability to add tags to each Article. And that means, we need a new relationship! Like always, we could add this by hand. But, the generator can help us. At your terminal, run:

php bin/console make:entity

But, hmm. Which entity should we update? We could add a new property to Article called tags or... I guess we could also add a new property to Tag called articles. That's really the same relationship, just viewed from two different sides.

And... yea! We could choose to update either class. The side you choose will make a subtle difference, and we'll learn about it soon. Let's update Article. For the property name, use tags. Remember, we need to stop thinking about the database and only think about our objects. In PHP, I want an Article object to have many Tag objects. So, the property should be called tags.

For type, use the fake relation type to activate the relationship wizard. We want to relate this to Tag and... perfect! Here is our menu of relationship options! I already hinted that this will be a ManyToMany relationship. But, let's look at the description to see if it fits. Each article can have many tags. And, each tag can relate to many articles. Yep, that's us! This is a ManyToMany relationship.

And just like last time, it asks us if we also want to map the other side of the relationship. This is optional, and is only for convenience. If we map the other side, we'll be able to say $tag->getArticles(). That may or may not be useful for us, but let's say yes. Call the field articles, because it will hold an array of Article objects.

And, that's it! Hit enter to finish.

Looking at the Generating Entities

Exciting! Let's see what changes this made. Open Article first:

247 lines | src/Entity/Article.php
// ... lines 1 - 15
class Article
{
// ... lines 18 - 68
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="articles")
*/
private $tags;
// ... lines 73 - 245
}

Yes! Here is the new tags property: it's a ManyToMany that points to the Tag entity. And, like we saw earlier with comments, whenever you have a relationship that holds many objects, in your constructor, you need to initialize that property to a new ArrayCollection:

247 lines | src/Entity/Article.php
// ... lines 1 - 15
class Article
{
// ... lines 18 - 73
public function __construct()
{
// ... line 76
$this->tags = new ArrayCollection();
}
// ... lines 79 - 245
}

The generator did that for us.

At the bottom, instead of a getter & setter, we have a getter, adder & remover:

247 lines | src/Entity/Article.php
// ... lines 1 - 15
class Article
{
// ... lines 18 - 220
/**
* @return Collection|Tag[]
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
}
return $this;
}
public function removeTag(Tag $tag): self
{
if ($this->tags->contains($tag)) {
$this->tags->removeElement($tag);
}
return $this;
}
}

There's no special reason for that: the adder & remover methods are just convenient.

Next, open Tag:

103 lines | src/Entity/Tag.php
// ... lines 1 - 13
class Tag
{
// ... lines 16 - 35
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="tags")
*/
private $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
// ... lines 45 - 74
/**
* @return Collection|Article[]
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles[] = $article;
$article->addTag($this);
}
return $this;
}
public function removeArticle(Article $article): self
{
if ($this->articles->contains($article)) {
$this->articles->removeElement($article);
$article->removeTag($this);
}
return $this;
}
}

The code here is almost identical: a ManyToMany pointing back to Article and, at the bottom, getter, adder & remover methods.

Owning Versus Inverse Sides

Great! But, which side is the owning side and which is the inverse side of the relationship? Open Comment:

96 lines | src/Entity/Comment.php
// ... lines 1 - 10
class Comment
{
// ... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
// ... lines 37 - 94
}

Remember, with a ManyToOne / OneToMany relationship, the ManyToOne side is always the owning side of the relation. That's easy to remember, because this is where the column lives in the database: the comment table has an article_id column.

But, with a ManyToMany relationship, well, both sides are ManyToMany! In Article, ManyToMany. In Tag, the same! So, which side is the owning side?

The answer lives in Article. See that inversedBy="articles" config?

247 lines | src/Entity/Article.php
// ... lines 1 - 15
class Article
{
// ... lines 18 - 68
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="articles")
*/
private $tags;
// ... lines 73 - 245
}

That points to the articles property in Tag. On the other side, we have mappedBy="tags", which points back to Article:

103 lines | src/Entity/Tag.php
// ... lines 1 - 13
class Tag
{
// ... lines 16 - 35
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="tags")
*/
private $articles;
// ... lines 40 - 101
}

Here's the point: with a ManyToMany relationship, you choose the owning side by where the inversedBy versus mappedBy config lives. The generator configured things so that Article holds the owning side because that's the entity we chose to update with make:entity.

Remember, all of this owning versus inverse stuff is important because, when Doctrine saves an entity, it only looks at the owning side of the relationship to figure out what to save to the database. So, if we add tags to an article, Doctrine will save that correctly. But, if you added articles to a tag and save, Doctrine would do nothing. Well, in practice, if you use make:entity, that's not true. Why? Because the generated code synchronizes the owning side. If you call $tag->addArticle(), inside, that calls $article->addTag():

103 lines | src/Entity/Tag.php
// ... lines 1 - 13
class Tag
{
// ... lines 16 - 82
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles[] = $article;
$article->addTag($this);
}
return $this;
}
// ... lines 92 - 101
}

Generating the Migration

Enough of that! Let's generate the migration:

php bin/console make:migration

Cool! Go open that file:

31 lines | src/Migrations/Version20180501143055.php
// ... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180501143055 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('CREATE TABLE article_tag (article_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_919694F97294869C (article_id), INDEX IDX_919694F9BAD26311 (tag_id), PRIMARY KEY(article_id, tag_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F97294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F9BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE');
}
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('DROP TABLE article_tag');
}
}

Woh! It creates a new table! Of course! That's how you model a ManyToMany relationship in a relational database. It creates an article_tag table with only two fields: article_id and tag_id.

This is very different than anything we've seen so far with Doctrine. This is the first time - and really, the only time - that you will have a table in the database, that has no direct entity class. This table is created magically by Doctrine to help us relate tags and articles. And, as we'll see next, Doctrine will also automatically insert and delete records from this table as we add and remove tags from an article.

Now, run the migration:

php bin/console doctrine:migrations:migrate

Let's go tag some articles!