Most Popular Answers Page
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet's build a "top answers" page where we list the answers with the most votes for all questions on our site.
Creating the Route, Controller & Template
Open AnswerController
and create a new public function called popularAnswers()
.
// ... lines 1 - 11 | |
class AnswerController extends AbstractController | |
{ | |
// ... lines 14 - 16 | |
public function popularAnswers() | |
{ | |
// ... line 19 | |
} | |
// ... lines 21 - 43 | |
} |
Add an @Route()
above this - or use the Route
attribute if you're on PHP 8 - with the URL /answers/popular
. Immediately give this a name so we can link to it: app_popular_answers
.
// ... lines 1 - 11 | |
class AnswerController extends AbstractController | |
{ | |
/** | |
* @Route("/answers/popular", name="app_popular_answers") | |
*/ | |
public function popularAnswers() | |
{ | |
// ... line 19 | |
} | |
// ... lines 21 - 43 | |
} |
Inside, render a template: answer/popularAnswers.html.twig
.
// ... lines 1 - 11 | |
class AnswerController extends AbstractController | |
{ | |
/** | |
* @Route("/answers/popular", name="app_popular_answers") | |
*/ | |
public function popularAnswers() | |
{ | |
return $this->render('answer/popularAnswers.html.twig'); | |
} | |
// ... lines 21 - 43 | |
} |
Now, copy that template name and, down in the templates/
directory, create the new answer/
folder... and inside, the new file: popularAnswers.html.twig
. I'll paste in a little structure to get us started.
{% extends 'base.html.twig' %} | |
{% block title %}Popular Answers{% endblock %} | |
{% block body %} | |
// ... lines 6 - 10 | |
{% endblock %} |
This extends base.html.twig
, overrides the title
block to customize the title... and in the body
block, adds some basic structure. Let's put an <h1>
that says "Most Popular Answers".
{% extends 'base.html.twig' %} | |
{% block title %}Popular Answers{% endblock %} | |
{% block body %} | |
<div class="container my-md-4"> | |
<div class="row"> | |
<h1>Most Popular Answers</h1> | |
</div> | |
</div> | |
{% endblock %} |
Before we try this, open up base.html.twig
so we can link to this. Scroll down a little. Inside of the navbar
, we have an empty <ul>
that's just waiting for a link. Add an <li>
with class="nav-item"
... and an a
tag inside with href
set to our new page: path('app_popular_answers')
. Say "Answers" for the link text... and this needs class="nav-link"
.
// ... line 1 | |
<html> | |
// ... lines 3 - 14 | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1" style="height: 60px;"> | |
// ... lines 17 - 20 | |
<div class="collapse navbar-collapse"> | |
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ path('app_popular_answers') }}">Answers</a> | |
</li> | |
</ul> | |
</div> | |
// ... lines 28 - 29 | |
</nav> | |
// ... lines 31 - 35 | |
</body> | |
</html> |
Now let's try this thing. Refresh... and click the link. Hello normal, boring, but functional page.
Querying for the Most Popular Answers
To get the most popular answers, we need a custom query. Well, technically we could use the findBy()
method on AnswerRepository
and use its "order by" argument. But let's add a full custom repository method instead: that will be nice and descriptive.
Open up AnswerRepository
. At the bottom, add the method. Let's call it findMostPopular()
and set the return type to an array. Like normal, I'll use PHPDoc to advertise that, more specifically, this will return an array of Answer
objects.
// ... lines 1 - 15 | |
class AnswerRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 40 | |
/** | |
* @return Answer[] Returns an array of Answer objects | |
*/ | |
public function findMostPopular(): array | |
{ | |
// ... lines 46 - 51 | |
} | |
} |
Inside, it's a simple query: return $this->createQueryBuilder('answer')
, ->addCriteria()
and reuse self::createApprovedCriteria()
so that this only returns approved answers.
// ... lines 1 - 15 | |
class AnswerRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 40 | |
/** | |
* @return Answer[] Returns an array of Answer objects | |
*/ | |
public function findMostPopular(): array | |
{ | |
return $this->createQueryBuilder('answer') | |
->addCriteria(self::createApprovedCriteria()) | |
// ... lines 48 - 51 | |
} | |
} |
Then ->orderBy('answer.votes', 'DESC')
, ->setMaxResults(10)
to only return the top 10 answers, ->getQuery()
, ->getResult()
.
// ... lines 1 - 15 | |
class AnswerRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 40 | |
/** | |
* @return Answer[] Returns an array of Answer objects | |
*/ | |
public function findMostPopular(): array | |
{ | |
return $this->createQueryBuilder('answer') | |
->addCriteria(self::createApprovedCriteria()) | |
->orderBy('answer.votes', 'DESC') | |
->setMaxResults(10) | |
->getQuery() | |
->getResult(); | |
} | |
} |
Beautiful! Back in the controller, autowire AnswerRepository $answerRepository
, and then we can say $answers = $answerRepository->findMostPopular()
.
// ... lines 1 - 5 | |
use App\Repository\AnswerRepository; | |
// ... lines 7 - 12 | |
class AnswerController extends AbstractController | |
{ | |
// ... lines 15 - 17 | |
public function popularAnswers(AnswerRepository $answerRepository) | |
{ | |
$answers = $answerRepository->findMostPopular(); | |
// ... lines 21 - 24 | |
} | |
// ... lines 26 - 48 | |
} |
Add a second argument to render()
so that we can pass an answers
variable to Twig set to this array of answers.
// ... lines 1 - 5 | |
use App\Repository\AnswerRepository; | |
// ... lines 7 - 12 | |
class AnswerController extends AbstractController | |
{ | |
// ... lines 15 - 17 | |
public function popularAnswers(AnswerRepository $answerRepository) | |
{ | |
$answers = $answerRepository->findMostPopular(); | |
return $this->render('answer/popularAnswers.html.twig', [ | |
'answers' => $answers | |
]); | |
} | |
// ... lines 26 - 48 | |
} |
In the template, add a ul
and loop over answers
with {% for answer in answers %}
. Let's start real simple: render answer.votes
so we can at least make sure that we have the most popular on top.
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container my-md-4"> | |
<div class="row"> | |
// ... lines 8 - 9 | |
<ul class="list-unstyled"> | |
{% for answer in answers %} | |
<li>{{ answer.votes }}</li> | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
{% endblock %} |
Spin over to your browser, refresh and... got it! 10 answers with the most highly voted on top.
Reusing the Answer Templates
So on the question show page, we already have a nice structure for rendering answers. I want to reuse this on our new popular answers page. Open question/show.html.twig
. Select everything inside the for
loop - the entire <li>
that renders a single answer - and copy it. Then, in the templates/answer/
directory, create a new file called _answer.html.twig
... and paste!
<li class="mb-4"> | |
<div class="row"> | |
<div class="col-1"> | |
<img src="{{ asset('images/tisha.png') }}" width="50" height="50" alt="Tisha avatar"> | |
</div> | |
<div class="col-9"> | |
{{ answer.content|parse_markdown }} | |
<p>-- {{ answer.username }}</p> | |
</div> | |
<div class="col-2 text-end"> | |
<small>{{ answer.createdAt|ago }}</small> | |
<div | |
class="vote-arrows" | |
{{ stimulus_controller('answer-vote', { | |
url: path('answer_vote', { | |
id: answer.id | |
}) | |
}) }} | |
> | |
<button | |
class="vote-up btn btn-link" | |
name="direction" | |
value="up" | |
{{ stimulus_action('answer-vote', 'clickVote') }} | |
><i class="far fa-arrow-alt-circle-up"></i></button> | |
<button | |
class="vote-down btn btn-link" | |
name="direction" | |
value="down" | |
{{ stimulus_action('answer-vote', 'clickVote') }} | |
><i class="far fa-arrow-alt-circle-down"></i></button> | |
<span><span {{ stimulus_target('answer-vote', 'voteTotal') }}>{{ answer.votes }}</span></span> | |
</div> | |
</div> | |
</div> | |
</li> |
Back in show.html.twig
, delete all of this and replace it with {{ include('answer/_answer.html.twig') }}
.
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
// ... lines 7 - 54 | |
<ul class="list-unstyled"> | |
{% for answer in question.approvedAnswers %} | |
{{ include('answer/_answer.html.twig') }} | |
{% endfor %} | |
</ul> | |
</div> | |
{% endblock %} |
Now copy that line and, in the popular answers template, repeat this! The new template includes the <li>
element... so this will fit perfectly inside of our ul
.
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container my-md-4"> | |
<div class="row"> | |
// ... lines 8 - 9 | |
<ul class="list-unstyled"> | |
{% for answer in answers %} | |
{{ include('answer/_answer.html.twig') }} | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
{% endblock %} |
Conditionally Rendering the Answer's Question
Phew! Let's check it! Refresh and... very nice! But hmm, in this context, we really need to render which question this answer is answering. We don't want to do that on the question show page - that would be redundant - but we do want it here.
To allow that, in popularAnswers.html.twig
, add a second argument to include()
and pass in a new variable called showQuestion
set to true
.
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container my-md-4"> | |
<div class="row"> | |
// ... lines 8 - 9 | |
<ul class="list-unstyled"> | |
{% for answer in answers %} | |
{{ include('answer/_answer.html.twig', { | |
showQuestion: true | |
}) }} | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
{% endblock %} |
In _answer.html.twig
, we can use that: if showQuestion|default(false)
and endif
. Thanks to the default
filter, if this variable is not passed, instead of an error, it'll default to false.
<li class="mb-4"> | |
{% if showQuestion|default(false) %} | |
// ... lines 3 - 11 | |
{% endif %} | |
<div class="row"> | |
// ... lines 14 - 45 | |
</div> | |
</li> |
Inside, add an <a>
tag with href=""
set to {{ path('app_question_show') }}
: the route to the question show page. This route needs a slug
parameter set to answer.question.slug
. Also give this some classes: mb-1
and link-secondary"
. For the text, say <strong>
"Question" and then print the question text: answer.question.question
.
<li class="mb-4"> | |
{% if showQuestion|default(false) %} | |
<a | |
href="{{ path('app_question_show', { | |
slug: answer.question.slug | |
}) }}" | |
class="mb-1 link-secondary" | |
> | |
<strong>Question:</strong> | |
{{ answer.question.question }} | |
</a> | |
{% endif %} | |
<div class="row"> | |
// ... lines 14 - 45 | |
</div> | |
</li> |
That does look funny, but... it's correct: answer.question
gives us the Question
object... then the last part reads its question
property.
Back at our browser, refresh and... yikes! That technically works but these questions are way too long! We need to shorten them!
Next, let's learn about Twig's powerful u
filter and add a method to our Answer
class that will make our code a whole lot more readable.
i get error (Unknown "stimulus_controller" function) please help me