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 SubscribeEach 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.
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.
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.
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.
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.
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!
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 (-:
// 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
}
}
Isn't querying question by slug slower than by id? Can we somehow keep slugs in urls and do queries by id? Just interesting.