Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Fetching Relations

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

Each published Question in the database will now be related to approximately 5 answers. Head to the homepage and click into a question. Time to replace this hardcoded craziness with real, dynamic answers.

Querying for Answers with findBy()

This means that we need to find all the answers for this specific Question. How can we do that? When we ran the make:entity command to create the Answer entity, it also generated an AnswerRepository class. And you might remember from the last tutorial that these repository classes have some nice, built-in methods for querying, like findBy() where we can find all the answers in the database that match some criteria, like WHERE votes = 5 or WHERE question_id = the id of some question.

... lines 1 - 2
namespace App\Repository;
use App\Entity\Answer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Answer|null find($id, $lockMode = null, $lockVersion = null)
* @method Answer|null findOneBy(array $criteria, array $orderBy = null)
* @method Answer[] findAll()
* @method Answer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AnswerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Answer::class);
}
... lines 21 - 49
}

Open the controller for this page: src/Controller/QuestionController.php... it's the show() action. Autowire the AnswerRepository service as an argument.

... lines 1 - 5
use App\Repository\AnswerRepository;
... lines 7 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question, AnswerRepository $answerRepository)
{
... lines 53 - 69
}
... lines 71 - 90
}

Then, below, say $answers = $answerRepository->findBy() and pass this an array that should be used to build the WHERE statement in the query. To find all the answers WHERE the question_id matches this question, pass question set to the $question object. Remember: by this point, Doctrine has already used the slug in the URL to query for the Question object.

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question, AnswerRepository $answerRepository)
{
... lines 53 - 56
$answers = $answerRepository->findBy(['question' => $question]);
... lines 58 - 69
}
... lines 71 - 90
}

The important thing here is that, when we call findBy(), we don't say 'question_id' => $question... or 'question' => $question->getId(). No! With Doctrine, we need to stop thinking about the database: we need to think only about the objects. We want to find all the Answer objects whose question property equals this $question object.

Behind the scenes, Doctrine will be smart enough to query WHERE the question_id column matches the id from this object.

Let's dump & die the $answers variable... and go see what it looks like. Refresh.

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question, AnswerRepository $answerRepository)
{
... lines 53 - 56
$answers = $answerRepository->findBy(['question' => $question]);
dd($answers);
... lines 59 - 69
}
... lines 71 - 90
}

Yes! This dumps an array of answers! Apparently this question is only related to two answers. Let's go pick a different one with more answers... cool! This question is related to four answers. That's, checks math, twice as interesting.

So... yay! Want to fetch all the Answer objects related to a Question? We just saw that you can do that by querying for the Answer entity and treating the question property like any normal property... except that you pass an entire Question object into the query.

Using the $question->getAnswers()

Now that we've done that... let's do something easier! Remove the AnswerRepository argument entirely... and instead say $answers = $question->getAnswers(). I'll put the dd($answers) back.

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question)
{
... lines 53 - 56
$answers = $question->getAnswers();
dd($answers);
... lines 59 - 69
}
... lines 71 - 90
}

When we ran the make:entity command, it asked us if we wanted to also add an $answers property to the Question class. We said yes, which generated some code that allows us to use this handy shortcut.

PersistentCollection & ArrayCollection

Over at the browser, when we refresh, we should see the same list of answers. And... we don't!? We get some PersistentCollection object. And, even stranger, I don't see the Answer objects anywhere inside of this collection. Dude, where's my answers?

Excellent question! Two important things here. First, remember that, inside the Question entity, the $answers property will not be a true array of Answer objects. Nope, it will be some sort of Doctrine collection object. It may be an ArrayCollection object or this PersistentCollection object... just depending on the situation. It doesn't really matter because both of these classes implement the same Collection interface... and both look and act like a normal array. The point is: that PersistentCollection is just an array-like wrapper around the answers... and not something we'll think about much.

Relations are Lazy-Loaded

The second thing to know is that when we query for a Question, Doctrine basically executes a SELECT * FROM question query. It grabs all the data from the question table and puts it onto the properties of the Question object. But, it does not immediately query the answer table for the related answers data. Nope, Doctrine doesn't query for the answers until - and unless - we actually use the $answers property. So, at this moment, it has not yet made the query for the answers data... which is why you don't see them inside this collection object. This feature is called "lazy loading".

Check this out: back in QuestionController, remove the dd()... and foreach over the $answers collection. Inside, do a normal dump() of the $answer variable.

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question)
{
... lines 53 - 56
$answers = $question->getAnswers();
foreach ($answers as $answer) {
dump($answer);
}
... lines 61 - 71
}
... lines 73 - 92
}

It's pretty crazy, but the moment that we foreach over the $answers collection - so the moment that we actually start using the answers data - Doctrine will query for that data.

We can see this! Refresh. Because we don't have a die() statement, the dump() shows up down in the web debug toolbar. And... yes! It found the same 4 answers!

Click the Doctrine icon on the toolbar to jump into its profiler... and look at the queries. There are two. First Doctrine queries for the question data. Then a moment later - at the moment the foreach line is executed - it queries FROM answer WHERE question_id = the id of this specific question. So, Doctrine lazily loads the answers data: it only makes the query once we force it to.

Anyways, we have answers! So next, let's pass these into the template, render their data, find an even easier way to do this and finally bring our answer-voting system to life by saving real vote totals to the database.

Leave a comment!

4
Login or Register to join the conversation
Ruslan I. Avatar
Ruslan I. Avatar Ruslan I. | posted 1 year ago

Isn't querying question by slug slower than by id? Can we somehow keep slugs in urls and do queries by id? Just interesting.

1 Reply

Hey Ruslan I.!

That's a good question :). The answer is no... as long as you have a "unique database index" on the slug field. We DO have this thanks to the unique=true option that we have on the @ORM\Column() for the slug field. That option adds the unique database index... and then querying is just as fast as querying by the id (or if there is some difference, it is infinitesimally small).

Cheers!

2 Reply
Ruslan I. Avatar

that's great. Thank you!

Reply
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago | edited

Just sayin'... loading the /questions/your-slug-here page will give you a fatal error about passing an Answer object rather than a string to the parse_markdown extension in the template. The fix is obvious enough -- {{ answer.<b>content</b> | parse_markdown }} -- but this isn't mentioned until the next chapter (-:

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