Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Saving Relations

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.

Our answer table has a new question_id column. Cool... but how do we populate that column? How do we relate an Answer to a Question? This is actually pretty easy... but it might feel weird if you're used to working with databases directly.

Open up src/DataFixtures/AppFixtures.php. We're using Foundry to add rich fixtures, or fake data, into our project.

... lines 1 - 2
namespace App\DataFixtures;
use App\Entity\Question;
use App\Factory\QuestionFactory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
QuestionFactory::createMany(20);
QuestionFactory::new()
->unpublished()
->many(5)
->create()
;
$manager->flush();
}
}

But to see how relationships work, let's do some good ol' fashioned manual coding.

Creating some Dummy Question and Answer Objects

Start by creating a new Answer object... and populate it with enough data to get it to save. Repeat this to create a new Question object... and also give that some data.

... lines 1 - 10
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 15 - 22
$answer = new Answer();
$answer->setContent('This question is the best? I wish... I knew the answer.');
$answer->setUsername('weaverryan');
$question = new Question();
$question->setName('How to un-disappear your wallet.');
$question->setQuestion('... I should not have done this...');
... lines 30 - 34
}
}

Save these boring objects to the database by calling $manager->persist() on both of them.

... lines 1 - 10
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 15 - 22
$answer = new Answer();
$answer->setContent('This question is the best? I wish... I knew the answer.');
$answer->setUsername('weaverryan');
$question = new Question();
$question->setName('How to un-disappear your wallet.');
$question->setQuestion('... I should not have done this...');
$manager->persist($answer);
$manager->persist($question);
... lines 33 - 34
}
}

Cool. If we stop now, these objects won't be related... and the Answer won't even save! Try it:

symfony console doctrine:fixtures:load

And... woh! My bad! We generated a migration in the last chapter, and then I totally forgot to run it! Time to do that:

symfony console doctrine:migrations:migrate

Now try the fixtures:

symfony console doctrine:fixtures:load

JoinColumn

That's the error I expected:

question_id cannot be null on the answer table

That's because we made question_id required: it was one of the questions that make:entity command asked us. Oh, and I can show you where this is configured. Open up the Answer class and find the question property. It's this JoinColumn(nullable=false): that makes the question_id column required.

... lines 1 - 10
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 15 - 22
$answer = new Answer();
$answer->setContent('This question is the best? I wish... I knew the answer.');
$answer->setUsername('weaverryan');
$question = new Question();
$question->setName('How to un-disappear your wallet.');
$question->setQuestion('... I should not have done this...');
$manager->persist($answer);
$manager->persist($question);
... lines 33 - 34
}
}

Relating an Answer to a Question

Anyways, the thing we want to know is: how can I relate this Answer to this Question? How do we say that the Answer belongs to the Question? It's as simple as $answer->setQuestion($question).

... lines 1 - 10
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 15 - 28
$question->setQuestion('... I should not have done this...');
$answer->setQuestion($question);
$manager->persist($answer);
... lines 34 - 36
}
}

Notice that we do not say $question->getId(). We're not passing the ID to the question property, we're setting the entire Question object onto the property. Doctrine will be smart enough to save these in the correct order: it'll save the question first, grab its new id, and use that to save the Answer.

To prove it, reload the fixtures:

symfony console doctrine:fixtures:load

Ok, no errors. Let's see what the database looks like. We can use the doctrine:query:sql command as an easy way to do this: SELECT * FROM answer.

symfony console doctrine:query:sql 'SELECT * FROM answer'

Yes! We have one answer in the database and its question_id is set to 103. Let's query for that question:

symfony console doctrine:query:sql 'SELECT * FROM question WHERE id = 103'

And... there it is!

The big takeaway here is this: in PHP, we just think about objects. We think:

Hey! I'd really like to relate this Answer object to this Question object.

Then, when we save these, Doctrine handles all the nitty gritty details of figuring out how to save that for us. The database is almost an implementation detail that we don't need to think about much.

Next: now that we've seen how to relate objects, let's update our fixtures to use Foundry. That will let us create a ton of fake questions and answers and relate them with very little code.

Leave a comment!

1
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": ">=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