Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The ManyToOne Relation

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Okay: we have a Question entity and table. We have an Answer entity and table. Yay for us! But what we really want to do is relate an Answer to a Question.

To do this... well... forget about Doctrine for a second. Let's just think about how this would look in a database. So: each answer belongs to a single question. We would normally model this in the database by adding a question_id column to the answer table that's a foreign key to the question's id. This would allow each question to have many answers and each answer to be related to exactly one question.

Ok! So... we need to add a new column to the answer table. The way we've done that so far in Doctrine is by adding a property to the entity class. And adding a relationship is no different.

Generating the Answer.question ManyToOne Property

So find your terminal and run:

symfony console make:entity

We need to update the Answer entity. Now, what should the new property be called... question_id? Actually, no. And this is one of the coolest, but trickiest things about Doctrine. Instead, call it simply question... because this property will hold an entire Question object... but more on that later.

For the type, use a "fake" type called relation. This starts a wizard that will guide us through the process of adding a relationship. What class should this new property relate to? Easy: the Question entity.

Ah, and now we see something awesome: a big table that explains the four types of relationships with an example of each of one. You can read through all of these, but the one we need is ManyToOne. Each Answer relates to one Question. And each Question can have many answers. That's... exactly what we want. Enter ManyToOne. This is actually the king of relationships: most of the time, this will be the one you want.

Is the Answer.question property allowed to be null? This is asking if we should be allowed to save an Answer to the database that is not related to a Question. For us, that's a "no". Every Answer must have a question... except... I guess... in the Hitchhiker's Guide to the Galaxy. Anyways, saying "no" will make the new column required in the database.

Mapping the "Other" Side of the Relation

This next question is super interesting:

Do you want to add a new property to Question so you can access/update Answer objects from it.

Here's the deal: every relationship can have two sides. Think about it: an Answer is related to one Question. But... you can also view the relationship from the other direction and say that a Question has many answers.

Regardless of whether we say "yes" or "no" to this question, we will be able to get and set the Question for an Answer. If we do say "yes", it simply means that we will also be able to access the relationship from the other direction... like by saying $question->getAnswers() to get all the answers for a given Question.

And... hey! Being able to say $question->getAnswers() sounds pretty handy! So let's say yes. There's no downside... except that this will generate a little bit more code.

What should that new property in the Question entity be called? Use the default answers.

Finally it asks a question about orphanRemoval. This is a bit more advanced... and you probably don't need it. If you do discover later that you need it, you can enable it manually inside your entity. I'll say no.

And... done! Hit enter one more time to exit the wizard.

Checking out the Entity Changes

Let's go see what this did! I committed before recording, so I'll run

git status

to check things out. Ooo, both entities were updated. Let's open Answer first... and... here's the new question property. It looks like any other property except that instead of having ORM\Column above it, it has ORM\ManyToOne and targets the Question entity.

... lines 1 - 11
class Answer
{
... lines 14 - 32
/**
* @ORM\Column(type="integer")
*/
private $votes = 0;
/**
* @ORM\ManyToOne(targetEntity=Question::class, inversedBy="answers")
* @ORM\JoinColumn(nullable=false)
*/
private $question;
... lines 43 - 95
}

Scroll to the bottom. Down here, it generated a normal getter and setter method.

... lines 1 - 11
class Answer
{
... lines 14 - 84
public function getQuestion(): ?Question
{
return $this->question;
}
public function setQuestion(?Question $question): self
{
$this->question = $question;
return $this;
}
}

Let's go look at the Question entity. If we scroll... beautiful: this now has an answers property, which is a OneToMany relationship.

... lines 1 - 14
class Question
{
... lines 17 - 51
/**
* @ORM\OneToMany(targetEntity=Answer::class, mappedBy="question")
*/
private $answers;
... lines 56 - 176
}

And... all the way at the bottom, it generated a getter and setter method. Oh, well, instead of setAnswers(), it generated addAnswer() and removeAnswer(), which are just a bit more convenient, especially in Symfony if you're using the form component or the serializer.

... lines 1 - 6
use Doctrine\Common\Collections\Collection;
... lines 8 - 14
class Question
{
... lines 17 - 147
/**
* @return Collection|Answer[]
*/
public function getAnswers(): Collection
{
return $this->answers;
}
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this);
}
return $this;
}
public function removeAnswer(Answer $answer): self
{
if ($this->answers->removeElement($answer)) {
// set the owning side to null (unless already changed)
if ($answer->getQuestion() === $this) {
$answer->setQuestion(null);
}
}
return $this;
}
}

The ArrayCollection Object

Head back up near the top of this class. The command also generated a constructor method so that it could initialize the answers property to some ArrayCollection object.

... lines 1 - 5
use Doctrine\Common\Collections\ArrayCollection;
... lines 7 - 14
class Question
{
... lines 17 - 56
public function __construct()
{
$this->answers = new ArrayCollection();
}
... lines 61 - 176
}

Ok, so we know that each Question will have many answers. So we know that the answers property will be an array... or some sort of collection. In Doctrine... for internal reasons, instead of setting the answers property to an array, it sets it to a Collection object. That's... not too important: the object looks an acts like an array - like, you can foreach over it. But it does have a few extra useful methods on it.

Anyways, whenever you have a relationship that holds a "collection" of other items, you need to initialize that property to an ArrayCollection in your constructor. If you use the make:entity command, this will always be done for you.

ManyToOne vs OneToMany

Oh, and I want to point something out. We generated a ManyToOne relationship. We can see this in the Answer entity. But... in the Question entity, it says OneToMany.

This is a key thing to understand: a ManyToOne relationship and a OneToMany relationship are not actually two different types of relationships. Nope: they described the same relationship... just from the two different sides.

Think about it: from the perspective of a Question, we have a "one question relates to many answers" relationship - a OneToMany. From the perspective of the Answer entity, that same relationship would be described as "many answers can relate to one question": a ManyToOne.

The point is: when you see these two relationships, realize that they are not two different things: they're the same one relation seen from opposite sides.

The answer_id Foreign Key Column

Anyways, we ran make:entity and it added one property to each class and a few methods. Nothing fancy. Time to generate the migration for this:

symfony console make:migration

Let's go peek at the new file! How cool is this???

... lines 1 - 2
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210902132832 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE answer ADD question_id INT NOT NULL');
$this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_9474526C1E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
$this->addSql('CREATE INDEX IDX_9474526C1E27F6BF ON answer (question_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_9474526C1E27F6BF');
$this->addSql('DROP INDEX IDX_9474526C1E27F6BF ON answer');
$this->addSql('ALTER TABLE answer DROP question_id');
}
}

It's adding a question_id column to the answer table! Doctrine is smart: we added a question property to the Answer entity. But in the database, it added a question_id column that's a foreign key to the id column in the question table. In other words, the table structure looks exactly like we expected!

The tricky, but honestly awesome thing, is that, in PHP, to relate an Answer to a Question, we're not going to set the Answer.question property to an integer id. Nope, we're going to set it to an entire Question object. Let's see exactly how to do that next.

Leave a comment!

3
Login or Register to join the conversation
MattHB Avatar

whenever you say 'I committed before recording this part'... #guiltyBadPracticeFeelings :D

Reply
weaverryan Avatar weaverryan | SFCASTS | posted 1 year ago | edited

Hey @disqus_uUZdE4XYyx!

We have an EasyAdmin tutorial that is, internally, nearly completed :). We will record it sometime in the next 2 months I would imagine. I'll ping Victor (he's helping me with the tutorial) to make sure that these are on there.

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

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