Buy Access to Course
18.

Saving a ManyToMany Relation + Joins

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 now know that a ManyToMany relationship works with the help of a join table. The question now is: how can we insert new records into that join table? How can I relate an Article to several tags?

The answer is exactly the same as our ManyToOne relation. Start by opening BaseFixture. At the bottom, I'm going to paste in a new protected function called getRandomReferences():

72 lines | src/DataFixtures/BaseFixture.php
// ... lines 1 - 9
abstract class BaseFixture extends Fixture
{
// ... lines 12 - 61
protected function getRandomReferences(string $className, int $count)
{
$references = [];
while (count($references) < $count) {
$references[] = $this->getRandomReference($className);
}
return $references;
}
}

We already have a getRandomReference() method that returns just one object:

72 lines | src/DataFixtures/BaseFixture.php
// ... lines 1 - 9
abstract class BaseFixture extends Fixture
{
// ... lines 12 - 41
protected function getRandomReference(string $className) {
if (!isset($this->referencesIndex[$className])) {
$this->referencesIndex[$className] = [];
foreach ($this->referenceRepository->getReferences() as $key => $ref) {
if (strpos($key, $className.'_') === 0) {
$this->referencesIndex[$className][] = $key;
}
}
}
if (empty($this->referencesIndex[$className])) {
throw new \Exception(sprintf('Cannot find any references for class "%s"', $className));
}
$randomReferenceKey = $this->faker->randomElement($this->referencesIndex[$className]);
return $this->getReference($randomReferenceKey);
}
// ... lines 61 - 70
}

This is the same, but you can pass it a class name and how many of those objects you want back. The objects you get back may or may not be a unique set. Hey, my method isn't perfect, but, it's good enough.

Next, in ArticleFixtures, this is where we'll set the relationship. And that means, we need to make sure that TagFixture is loaded first so that the tags actually exist. At the top, add implements DependentFixtureInterface:

80 lines | src/DataFixtures/ArticleFixtures.php
// ... lines 1 - 7
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
// ... lines 9 - 10
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
// ... lines 13 - 78
}

Then, I'll go to the "Code"->"Generate" menu - or Command+N on a Mac - select "Implement Methods" and choose getDependencies(). We now depend on TagFixture::class:

80 lines | src/DataFixtures/ArticleFixtures.php
// ... lines 1 - 7
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
// ... lines 9 - 10
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
// ... lines 13 - 72
public function getDependencies()
{
return [
TagFixture::class,
];
}
}

Above, let's first get some tag objects: $tags = $this->getRandomReferences() and pass it Tag::class, and then, let's fetch $this->faker->numberBetween() zero and five. So, find 0 to 5 random tags:

80 lines | src/DataFixtures/ArticleFixtures.php
// ... lines 1 - 10
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
// ... lines 13 - 29
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
// ... lines 33 - 63
$tags = $this->getRandomReferences(Tag::class, $this->faker->numberBetween(0, 5));
// ... lines 65 - 67
});
// ... lines 69 - 70
}
// ... lines 72 - 78
}

And just to make sure you that this does give us Tag objects, dump($tags) and die. Now, find your terminal and run:

php bin/console doctrine:fixtures:load

Explaining Proxies

Perfect! These are Tag objects. Oh, by the way, sometimes you may notice that your entity's class name is prefixed by this weird Proxies stuff. When you see that, ignore it. A "Proxy" is a special class that Doctrine generates and sometimes wraps around your real entity objects. Doctrine does this so that it can perform its relationship lazy-loading magic.

Actually, check this out: it looks like all the data on this Tag is null! But, that's a lie! As soon as you reference any data on that Tag, Doctrine will query for the data and fill it in. That's lazy-loading in action.

Let me show you: add a foreach over $tags as $tag:

80 lines | src/DataFixtures/ArticleFixtures.php
// ... lines 1 - 10
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
// ... lines 13 - 29
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
// ... lines 33 - 63
$tags = $this->getRandomReferences(Tag::class, $this->faker->numberBetween(0, 5));
foreach ($tags as $tag) {
// ... line 66
}
});
// ... lines 69 - 70
}
// ... lines 72 - 78
}

To help PhpStorm, I'll use some inline PHPDoc to tell it that $tags is an array of Tag objects.

Inside the loop, just say $tag->getName():

// ...
$tags = $this->getRandomReferences(Tag::class, $this->faker->numberBetween(0, 5));
foreach ($tags as $tag) {
    $tag->getName();
    dump($tag);
}
die;
// ...

I know, that looks weird: we're calling a method but not using it! But, calling this method is enough to make Doctrine query for the tag's real data. Below, dump($tag) and die after the loop.

Load the fixtures again:

php bin/console doctrine:fixtures:load

Boom! We have data!

Anyways, this is the proxy system in action. You need to know what it is because you will see it from time-to-time. But mostly, it should be completely invisible: don't think about it.

Adding Tags to Article

Finally, how can we add each Tag to the Article? No surprise, it's $article->addTag():

80 lines | src/DataFixtures/ArticleFixtures.php
// ... lines 1 - 10
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
// ... lines 13 - 29
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
// ... lines 33 - 63
$tags = $this->getRandomReferences(Tag::class, $this->faker->numberBetween(0, 5));
foreach ($tags as $tag) {
$article->addTag($tag);
}
});
// ... lines 69 - 70
}
// ... lines 72 - 78
}

Try the fixtures again:

php bin/console doctrine:fixtures:load

Ok, no errors. To the database!

php bin/console doctrine:query:sql 'SELECT * FROM tag'

Yep, 10 tags with various, weird names. Let's see what the join table looks like:

php bin/console doctrine:query:sql 'SELECT * FROM article_tag'

Yea! 24 rows! Each time we add a Tag to Article and save, Doctrine inserts a row in this table. If we were to remove an existing Tag from an Article object - with $article->removeTag() - and then flush, Doctrine would actually delete that row. For the first, and only, time, we have a table that we don't need to think about, at all: Doctrine inserts and deletes data for us.

All we need to do is worry about relating Article objects to Tag objects. Doctrine handles the saving.

Rendering the Tags

And now we can turn back to building our site: open the article/show.html.twig template. On this page, let's print the tags right under the article title. So, scroll down a bit. Copy the span for the heart count and paste it below.

Because the Article object holds an array of tags, use for tag in article.tags:

86 lines | templates/article/show.html.twig
// ... lines 1 - 4
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<img class="show-article-img" src="{{ asset(article.imagePath) }}">
<div class="show-article-title-container d-inline-block pl-3 align-middle">
// ... lines 10 - 19
<span class="pl-2 article-details">
{% for tag in article.tags %}
// ... line 22
{% endfor %}
</span>
</div>
</div>
</div>
// ... lines 28 - 78
{% endblock %}
// ... lines 80 - 86

Inside, let's create a cute little badge and print the tag name: {{ tag.name }}:

86 lines | templates/article/show.html.twig
// ... lines 1 - 4
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<img class="show-article-img" src="{{ asset(article.imagePath) }}">
<div class="show-article-title-container d-inline-block pl-3 align-middle">
// ... lines 10 - 19
<span class="pl-2 article-details">
{% for tag in article.tags %}
<span class="badge badge-secondary">{{ tag.name }}</span>
{% endfor %}
</span>
</div>
</div>
</div>
// ... lines 28 - 78
{% endblock %}
// ... lines 80 - 86

Super cool! Try it: find the page and refresh. We got it! Well, this Article only has one tag - boring. Find a different one: boom! Four different tags.

Repeat this on the homepage: we'll list the tags right under the title. Copy the for loop, then open homepage.html.twig. Down below, add a <br>, then paste! Wrap this in a <small> tag and change the class to badge-light:

65 lines | templates/article/homepage.html.twig
// ... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
// ... lines 10 - 18
<!-- Supporting Articles -->
{% for article in articles %}
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
<img class="article-img" src="{{ asset(article.imagePath) }}">
<div class="article-title d-inline-block pl-3 align-middle">
// ... lines 26 - 27
<br>
{% for tag in article.tags %}
<small>
<span class="badge badge-light">{{ tag.name }}</span>
</small>
{% endfor %}
// ... lines 34 - 36
</div>
</a>
</div>
{% endfor %}
</div>
// ... lines 42 - 61
</div>
</div>
{% endblock %}

This is just the same thing again: we have an article variable, which allows us to easily loop over its tags.

But notice, before we refresh, there are 8 queries. But now... there are 15! The page works, but we have another N+1 query problem. And, it's probably no big deal, but let's learn how to add a JOIN to a ManyToMany query so that we can fix it.