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 just successfully generated a ManyToMany relationship between Question
and Tag
... and we even made and executed the migration.
Now let's see how we can relate these objects in PHP. Open up src/DataFixtures/AppFixtures.php
. We're going to create a couple of objects by hand. Start with $question = QuestionFactory::createOne()
to create a question - the lazy way - using our factory. Then I'll paste in some code that creates two Tag
objects for some very important topics to my 4 year old son.
... lines 1 - 6 | |
use App\Entity\Tag; | |
... lines 8 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 17 - 35 | |
$question = QuestionFactory::createOne(); | |
$tag1 = new Tag(); | |
$tag1->setName('dinosaurs'); | |
$tag2 = new Tag(); | |
$tag2->setName('monster trucks'); | |
... lines 42 - 45 | |
$manager->flush(); | |
} | |
} |
To actually save these, we need to call $manager->persist($tag1)
and $manager->persist($tag2)
.
... lines 1 - 6 | |
use App\Entity\Tag; | |
... lines 8 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 17 - 35 | |
$question = QuestionFactory::createOne(); | |
$tag1 = new Tag(); | |
$tag1->setName('dinosaurs'); | |
$tag2 = new Tag(); | |
$tag2->setName('monster trucks'); | |
$manager->persist($tag1); | |
$manager->persist($tag2); | |
$manager->flush(); | |
} | |
} |
Awesome! Right now, this will create one new Question
and two new tags... but they won't be related in the database. So how do we relate them? Well, don't think at all about the join table that was created... you really want to pretend like that doesn't even exist. Instead, like we've done with the other relationship, just think:
I want to relate these two
Tag
objects to thisQuestion
object.
Doing that is pretty simple: $question->addTag($tag1)
and $question->addTag($tag2)
.
... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 17 - 35 | |
$question = QuestionFactory::createOne(); | |
$tag1 = new Tag(); | |
$tag1->setName('dinosaurs'); | |
$tag2 = new Tag(); | |
$tag2->setName('monster trucks'); | |
$question->addTag($tag1); | |
$question->addTag($tag2); | |
... lines 45 - 49 | |
} | |
} |
That's it! Let's try this thing! Reload the fixtures:
symfony console doctrine:fixtures:load
And... no errors! Check the database:
symfony console doctrine:query:sql 'SELECT * FROM tag'
No surprise: we have two tags in this table. Now SELECT * FROM question_tag
- the join table.
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
And... yes! This has two rows! The first relates the first tag to the question and the second relates the second tag to that same question. How cool is that? We simply relate the objects in PHP and Doctrine handles inserting the rows into the join table.
If we saved all of this stuff and then, down here, said $question->removeTag($tag1)
and saved again, this would cause Doctrine to delete the first row in that table. All of the inserting and deleting happens automatically.
By the way, like with any relationship, a ManyToMany
has an owning side and an inverse side. Because we originally modified the Question
entity and added a $tags
property, this is the owning side.
In a ManyToOne
and OneToMany
relationship, the owning side is always the ManyToOne
... because that's the entity where the foreign key column exists, like question_id
on the answer
table.
But a ManyToMany
is a bit different: you get to choose which side is the owning side. Because we decided to update the Question
entity when we ran make:entity
, that command set up this class to be the owning side. The way you know is that it points to the other side by saying inversedBy=""
. So it's pointing to the other side of the relationship as the inverse side.
... lines 1 - 16 | |
class Question | |
{ | |
... lines 19 - 59 | |
/** | |
* @ORM\ManyToMany(targetEntity=Tag::class, inversedBy="questions") | |
*/ | |
private $tags; | |
... lines 64 - 214 | |
} |
Then, over in Tag
, this is the inverse side. And you can see that it says mappedBy="tags"
. This says:
The owning side - or "mapped side" - is the
tags
property over in theQuestion
entity.
... lines 1 - 13 | |
class Tag | |
{ | |
... lines 16 - 29 | |
/** | |
* @ORM\ManyToMany(targetEntity=Question::class, mappedBy="tags") | |
*/ | |
private $questions; | |
... lines 34 - 82 | |
} |
But... remember: this distinction isn't that important. Technically speaking, when we want to relate a Tag
and Question
, the only way to do that is by setting the owning side: setting the $tags
property on Question
.
So let's do an experiment: change the code to be $tag1->addQuestion($question)
and $tag2->addQuestion($question)
.
... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 17 - 42 | |
$tag1->addQuestion($question); | |
$tag2->addQuestion($question); | |
... lines 45 - 49 | |
} | |
} |
So we're now setting the inverse side of the relationship only. In theory, this should not save correctly. But let's try it: reload the fixtures.
symfony console doctrine:fixtures:load
Ah! This error is unrelated: it's from Foundry: it says that $tag->addQuestion()
argument one
should be a Question
object, but it received a Proxy
object.
When you create an object with Foundry, like up here, it actually returns a Proxy
object that wraps the true Question
object. It doesn't normally matter, but if you start mixing Foundry code with non-Foundry code, sometimes you can get this error. To fix it, add ->object()
.
... lines 1 - 12 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 17 - 35 | |
$question = QuestionFactory::createOne()->object(); | |
... lines 37 - 49 | |
} | |
} |
This will now be a pure Question
object.
Anyways, reload the fixtures again:
symfony console doctrine:fixtures:load
And... it works. More importantly, if we query the join table:
symfony console doctrine:query:sql 'SELECT * FROM question_tag'
We still have two rows! That means that we were able to relate Tag
and Question
object by setting only the inverse side of the relation... which is exactly the opposite of what I said.
But... this only works because our entity code is smart. Look at the Tag
class... and go down to the addQuestion()
method.
... lines 1 - 13 | |
class Tag | |
{ | |
... lines 16 - 64 | |
public function addQuestion(Question $question): self | |
{ | |
if (!$this->questions->contains($question)) { | |
$this->questions[] = $question; | |
$question->addTag($this); | |
} | |
return $this; | |
} | |
... lines 74 - 82 | |
} |
Yep, it calls $question->addTag($this)
. We saw this exact same thing with the Question
Answer
relationship. When we call, addQuestion()
, it handles setting the owning side of the relationship. That is why this saved. Watch: if we comment this line out...
... lines 1 - 13 | |
class Tag | |
{ | |
... lines 16 - 64 | |
public function addQuestion(Question $question): self | |
{ | |
if (!$this->questions->contains($question)) { | |
$this->questions[] = $question; | |
//$question->addTag($this); | |
} | |
return $this; | |
} | |
... lines 74 - 82 | |
} |
reload the fixtures...
symfony console doctrine:fixtures:load
... and query the join table, it's empty! We do have 2 Tag
objects... but they are not related to any questions in the database because we never set the owning side of the relationship. So... let's put that code back.
... lines 1 - 13 | |
class Tag | |
{ | |
... lines 16 - 64 | |
public function addQuestion(Question $question): self | |
{ | |
if (!$this->questions->contains($question)) { | |
$this->questions[] = $question; | |
$question->addTag($this); | |
} | |
return $this; | |
} | |
... lines 74 - 82 | |
} |
Next: let's use Foundry to create a bunch of Tag
objects and randomly relate them to questions.
Hey Ccc123,
Looks like you nailed it, good job! But you don't leverage Symfony Forms in your code completely :) I mean, you use Symfony Forms to render the form but you don't handle the form in Symfony way. We have a separate course about Symfony forms: https://symfonycasts.com/sc... - I think you might be interested in it. In particular, here's an example how to handle Symfony forms in controllers: https://symfonycasts.com/sc...
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.3
"doctrine/doctrine-bundle": "^2.1", // 2.4.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.9.5
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.7
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.3.*", // v5.3.7
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/validator": "5.3.*", // v5.3.14
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"twig/extra-bundle": "^2.12|^3.0", // v3.3.1
"twig/string-extra": "^3.3", // v3.3.1
"twig/twig": "^2.12|^3.0" // v3.3.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.33.0
"symfony/var-dumper": "5.3.*", // v5.3.7
"symfony/web-profiler-bundle": "5.3.*", // v5.3.5
"zenstruck/foundry": "^1.1" // v1.13.1
}
}
Hiya,
I was testing out the ManyToMany relationship using my own created examples (Post and Category) and I was wondering if this was the correct way of saving it to the db with a form.
What I did was made a post creation form and rendered a list of category names (checkboxes) from the db using EntityType. Then I processed it in PostController by looking up the the category based on the ID(s) that was coming from the POST request and saving it by using $post->addCategory(...).
Post creation form: https://i.imgur.com/hDE0sK3...
Post controller: https://i.imgur.com/UNI6MZn...