Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Making Questions owned by Users

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our 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!

Generating the Relationship

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');
}
}

Fixing the Migration

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

Assigning Owners in the Fixtures

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.

Leave a comment!

0
Login or Register to join the conversation
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": "^7.4.1 || ^8.0.0",
        "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
    }
}