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 SubscribeOur site has users and these questions are created by those users. So in the database, each Question
needs to be related to the User
that created it via a Doctrine relationship. Right now, if you open src/Entity/Question.php
, that is not the case. There's nothing that relates this back to the User
that created it. Time to fix that. We'll need this so we can properly talk about voters!
Find your terminal and run:
symfony console make:entity
We're going to modify the Question
entity and add a new property called owner
, which will be the "user" that owns this Question
. We need a ManyToOne relationship. If you're ever not sure, just type "relation" and it will guide you through a wizard to help. This will be a relation to the User
class... and the owner
property will not be nullable: every question must be owned by some user.
Next it asks if we want to map the other side of the relationship so that we can say $user->getQuestions()
. That might be handy, so let's say yes. And call that property questions
. Finally, I'm going to say no to orphan removal. And... done!
If you went through our Doctrine relationships tutorial, then you know that there's nothing special here. This added a ManyToOne
relationship above a new $owner
property... and made getter and setter methods at the bottom:
... lines 1 - 16 | |
class Question | |
{ | |
... lines 19 - 64 | |
/** | |
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="questions") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $owner; | |
... lines 70 - 227 | |
public function getOwner(): ?User | |
{ | |
return $this->owner; | |
} | |
public function setOwner(?User $owner): self | |
{ | |
$this->owner = $owner; | |
return $this; | |
} | |
} |
Over in the User
class, it also mapped the inverse side of the relationship:
... lines 1 - 5 | |
use Doctrine\Common\Collections\ArrayCollection; | |
use Doctrine\Common\Collections\Collection; | |
... lines 8 - 17 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 20 - 51 | |
/** | |
* @ORM\OneToMany(targetEntity=Question::class, mappedBy="owner") | |
*/ | |
private $questions; | |
public function __construct() | |
{ | |
$this->questions = new ArrayCollection(); | |
} | |
... lines 61 - 190 | |
/** | |
* @return Collection|Question[] | |
*/ | |
public function getQuestions(): Collection | |
{ | |
return $this->questions; | |
} | |
public function addQuestion(Question $question): self | |
{ | |
if (!$this->questions->contains($question)) { | |
$this->questions[] = $question; | |
$question->setOwner($this); | |
} | |
return $this; | |
} | |
public function removeQuestion(Question $question): self | |
{ | |
if ($this->questions->removeElement($question)) { | |
// set the owning side to null (unless already changed) | |
if ($question->getOwner() === $this) { | |
$question->setOwner(null); | |
} | |
} | |
return $this; | |
} | |
} |
Let's go make a migration for this change:
symfony console make:migration
And... as usual, we'll run over to the new migration file... to make sure it contains only the stuff we expect. Yep: ALTER TABLE question
, add owner_id
and then the foreign key stuff:
... lines 1 - 12 | |
final class Version20211012184326 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE question ADD owner_id INT NOT NULL'); | |
$this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494E7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)'); | |
$this->addSql('CREATE INDEX IDX_B6F7494E7E3C61F9 ON question (owner_id)'); | |
} | |
public function down(Schema $schema): void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494E7E3C61F9'); | |
$this->addSql('DROP INDEX IDX_B6F7494E7E3C61F9 ON question'); | |
$this->addSql('ALTER TABLE question DROP owner_id'); | |
} | |
} |
Let's run that:
symfony console doctrine:migrations:migrate
And... it failed! That's okay. It fails because there are already rows in the question
table. So adding a new owner_id
NOT NULL
makes those existing records... explode. In the Doctrine relations tutorial, we talked about how to responsibly handle, fix, and test failed migrations. Because we already talked about it there, I'm going to take the easy route here and just drop our database:
symfony console doctrine:database:drop --force
Then create a fresh database:
symfony console doctrine:database:create
And migrate again.
symfony console doctrine:migrations:migrate
Now it works. Reload the fixtures:
symfony console doctrine:fixtures:load
And... that exploded too! Come on! The insert into question is failing because owner_id
cannot be null. That makes sense: we haven't - yet - gone into our fixtures and given each question an owner.
Let's do that. Open src/Factory/QuestionFactory.php
. Our job in getDefaults()
, is to supply a default value for every required property. So I'm now going to add an owner
key set to UserFactory::new()
:
... lines 1 - 28 | |
final class QuestionFactory extends ModelFactory | |
{ | |
... lines 31 - 42 | |
protected function getDefaults(): array | |
{ | |
return [ | |
... lines 46 - 52 | |
'owner' => UserFactory::new(), | |
]; | |
} | |
... lines 56 - 68 | |
} |
Thanks to this, if we execute QuestionFactory
without overriding any variables, this will create a brand new user for each new question.
But inside of our fixtures, that's... not exactly what we want. Head down to the bottom where we create the users. What I want to do is create these users first. And then, when we create the questions up here... oh actually right here, I want to use a random user from the ones that we already created.
To do this, we first need to move our users up to the top so that they're created first:
... lines 1 - 15 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
UserFactory::createOne([ | |
'email' => 'abraca_admin@example.com', | |
'roles' => ['ROLE_ADMIN'] | |
]); | |
UserFactory::createOne([ | |
'email' => 'abraca_user@example.com', | |
]); | |
UserFactory::createMany(10); | |
TagFactory::createMany(100); | |
... lines 30 - 61 | |
} | |
} |
Then, down here for our main questions, pass a function to the second argument and return an array... so that we can override the owner
property. Set it to UserFactory::random()
:
... lines 1 - 15 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 20 - 30 | |
$questions = QuestionFactory::createMany(20, function() { | |
return [ | |
'owner' => UserFactory::random(), | |
]; | |
}); | |
... lines 36 - 61 | |
} | |
} |
I'm not going to worry about also doing this for the unpublished questions down here... but we could.
Ok: let's try the fixtures again:
symfony console doctrine:fixtures:load
This time... they work!
Cool! So let's leverage the new relationship on our site to print the real owner of each question. We're also going to start a question edit page and then... have to figure out how to make it so that only the owner of each question can access it.
Hey @Sahetmyrat ,
The Question::$user
field is supposed to be a real User
object while you're trying to set a user ID only. You need to pass the whole User
object here, i.e. do $question->setOwner($user);
.
Cheers!
Hey Sahetmyrat,
Awesome! :) Yeah, Doctrine operates objects because it's ORM, so you need to pass the whole object to it instead of just an ID
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.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"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
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"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.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"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.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Hello Ryan. I wrote route for creating new question, and I don't know correct way to insert owner to
owner_id
in my controller. But I tried this:$userId = $user->getId(); $question->setOwner($userId);
, and givesUnable to guess how to get a Doctrine instance from the request information for parameter "user".
Please give me hint to solve that. Thank you!