If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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
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.
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.
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.
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
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!
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 ?
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!
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!
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!
// 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
}
}
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...