gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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.
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.
This next question is super interesting:
Do you want to add a new property to
Question
so you can access/updateAnswer
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.
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; | |
} | |
} |
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.
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.
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.
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!
// 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
}
}
whenever you say 'I committed before recording this part'... #guiltyBadPracticeFeelings :D