Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Saving a ManyToMany Relation + Joins

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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():

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

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

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

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

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

... 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():

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

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

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

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

Leave a comment!

8
Login or Register to join the conversation
Mohamed K. Avatar
Mohamed K. Avatar Mohamed K. | posted 4 years ago

I get an error "cannot find any references for class "App\Entity\Article"
while trying to the command ./bin/console doctrine:fixtures:load
I followed the steps and I understand the function and how it works but I can't seem to find where I messed up...

1 Reply
Tomasz P. Avatar

When running doctrine:fixtures:load, check on what file it stopped.
For my it was App\DataFixtures\CommentFixture.

To fix it just added dependent fixture:
Add getDependencies to App\DataFixtures\CommentFixture (also add implements DependentFixtureInterface).

So basicly:

class CommentFixture extends BaseFixture implements DependentFixtureInterface
{
...
public function getDependencies()
{
return [ArticleFixtures::class];
}
}

Hope it helps

Reply

Hey Tomek,

Makes sense, thanks for this tip!

Cheers!

Reply

Hey @Bob

I believe your problem is related to a dependent fixture not being load in the right order. Could you double check it?

Cheers!

Reply
Default user avatar
Default user avatar Kristof Kreimeyer | posted 4 years ago

I get an error after implementing the code in ArticleFixtures. When i execute ./bin/console doctrine:fixtures:load the error " Cannot find any references for class "App\Entity\Article"" appears.

What did i do wrong ?

Reply

Hey Kristof,

This is our custom exception, you can see it here: https://symfonycasts.com/sc... . So, if you get this - it means you call getRandomReference() for App\Entity\Article but this->referencesIndex array for this class is empty. Actually, see this chapter: https://symfonycasts.com/sc... - I bet you have invalid fixtures order, we had a similar problem there and have fixed it.

Let me know if it does not help.

Cheers!

Reply
Default user avatar
Default user avatar Alexander Enlund | posted 5 years ago

Hey! I'm having a problem with the getRandomReferences function, I don't really understand what's going on, what is the difference between getRandomReference and getRandomReferences function and what should it return?
AND
should this look like:
$tags = $this->getRandomReferences(Tag::class);
$this->faker->numberBetween(0, 5);
or is it totally wrong?

THANKS!

Reply

Hey Alexander,

Based on the name we can see getRandomReference() returns only one object while getRandomReferences() returns many, i.e. returns an array of the objects. Ryan prepare that function for you, so you just need to copy/paste it and you'll see the code and what exactly it returns. So, let's wait for this video to be released, and I bet you will understand the difference easily ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice