Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Rendering Answer Data & Saving Votes

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

So let's render some answer data! Delete the old, hardcoded $answers and the foreach. Perfect: we're now passing this collection of Answer objects into the template:

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question)
{
... lines 53 - 56
$answers = $question->getAnswers();
return $this->render('question/show.html.twig', [
'question' => $question,
'answers' => $answers,
]);
}
... lines 64 - 83
}

Let's go open this template... because it'll probably need a few tweaks: templates/question/show.html.twig.

If you scroll down a bit - here it is - we loop over the answers variable. That will still work: the Doctrine collection is something that we can loop over. But the answer variable will now be an Answer object. So, to get the content, use answer.content:

... lines 1 - 4
{% block body %}
... lines 6 - 54
<ul class="list-unstyled">
{% for answer in answers %}
<li class="mb-4">
<div class="row">
... lines 59 - 61
<div class="col-9">
{{ answer.content|parse_markdown }}
... line 64
</div>
... lines 66 - 89
</div>
</li>
{% endfor %}
</ul>
... line 94
{% endblock %}

We can also remove the hardcoded username and replace it with answer.username:

... lines 1 - 4
{% block body %}
... lines 6 - 54
<ul class="list-unstyled">
{% for answer in answers %}
<li class="mb-4">
<div class="row">
... lines 59 - 61
<div class="col-9">
{{ answer.content|parse_markdown }}
<p>-- {{ answer.username }}</p>
</div>
... lines 66 - 89
</div>
</li>
{% endfor %}
</ul>
... line 94
{% endblock %}

And there's... one more spot. The vote count is hardcoded. Change that to answer.votes:

... lines 1 - 4
{% block body %}
... lines 6 - 54
<ul class="list-unstyled">
{% for answer in answers %}
<li class="mb-4">
<div class="row">
... lines 59 - 61
<div class="col-9">
{{ answer.content|parse_markdown }}
<p>-- {{ answer.username }}</p>
</div>
<div class="col-2 text-end">
<div
... lines 68 - 73
>
... lines 75 - 86
<span><span {{ stimulus_target('answer-vote', 'voteTotal') }}>{{ answer.votes }}</span></span>
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
... line 94
{% endblock %}

Ok! Let's see how it looks. Refresh and... alright! We have dynamic answers!

Fetching the Answers Directly in Twig

But... we're still doing too much work! Head back to the controller and completely remove the $answers variable:

... lines 1 - 15
class QuestionController extends AbstractController
{
... lines 18 - 50
public function show(Question $question)
{
if ($this->isDebug) {
$this->logger->info('We are in debug mode!');
}
return $this->render('question/show.html.twig', [
'question' => $question,
]);
}
... lines 61 - 80
}

Why are we doing this? Well, we know that we can say $question->getAnswers() to get all the answers for a question. And since we're passing a $question object into the template... we can call that method directly from Twig!

In show.html.twig, we don't have an answers variable anymore. That's ok because we can say question.answers:

... lines 1 - 4
{% block body %}
... lines 6 - 54
<ul class="list-unstyled">
{% for answer in question.answers %}
... lines 57 - 91
{% endfor %}
</ul>
... line 94
{% endblock %}

As reminder, when we say question.answers, Twig will first try to access the $answers property directly. But because it's private, it will then call the getAnswers() method. In other words, this is calling the same code that we were using a few minutes ago in our controller.

Back in the template, we need to update one more spot: the answer|length that renders the number of answers. Change this to question.answers:

... lines 1 - 4
{% block body %}
... lines 6 - 47
<div class="d-flex justify-content-between my-4">
<h2 class="">Answers <span style="font-size:1.2rem;">({{ question.answers|length }})</span></h2>
... line 50
</div>
... lines 52 - 94
{% endblock %}

Refresh now and... we're still good! If you open the Doctrine profiler, we have the same 2 queries. But now this second query is literally being made from inside of the Twig template.

Saving Answer Votes

While we're here, in the first Symfony 5 tutorial, we wrote some JavaScript to support this answer voting feature. When we click, it... well... sort of works? It makes an Ajax call: we can see that down on the toolbar. But since there were no answers in the database when we built this, we... just "faked" it and returned a new random vote count from the Ajax call. Now we can make this actually work!

Before I recorded this tutorial, I refactored the JavaScript logic for this into Stimulus. If you want to check that out, it lives in assets/controllers/answer-vote_controller.js:

import { Controller } from 'stimulus';
import axios from 'axios';
... lines 3 - 12
export default class extends Controller {
static targets = ['voteTotal'];
static values = {
url: String,
}
clickVote(event) {
event.preventDefault();
const button = event.currentTarget;
axios.post(this.urlValue, {
data: JSON.stringify({ direction: button.value })
})
.then((response) => {
this.voteTotalTarget.innerHTML = response.data.votes;
})
;
}
}

The important thing for us is that, when we click the vote button, it makes an Ajax call to src/Controller/AnswerController.php: to the answerVote method. Inside, yup! We're grabbing a random number, doing nothing with it, and returning it.

To make the voting system truly work, start in show.html.twig. The way that our Stimulus JavaScript knows what URL to send the Ajax call to is via this url variable that we pass into that controller. It's generating a URL to the answer_vote route... which is the route above the target controller. Right now, for the id wildcard... we're passing in a hardcoded 10. Change that to answer.id:

... lines 1 - 4
{% block body %}
... lines 6 - 54
<ul class="list-unstyled">
{% for answer in question.answers %}
<li class="mb-4">
<div class="row">
... lines 59 - 65
<div class="col-2 text-end">
<div
class="vote-arrows"
{{ stimulus_controller('answer-vote', {
url: path('answer_vote', {
id: answer.id
})
}) }}
>
... lines 75 - 87
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
... line 94
{% endblock %}

Back in the controller, we need to take this id and query for the Answer object. The laziest way to do that is by adding an Answer $answer argument. Doctrine will see that entity type-hint and automatically query for an Answer where id equals the id in the URL.

Remove this TODO stuff... and for the "up" direction, say $answer->setVotes($answer->getVotes() + 1). Use the same thing for the down direction with minus one.

... lines 1 - 11
class AnswerController extends AbstractController
{
... lines 14 - 16
public function answerVote(Answer $answer, LoggerInterface $logger, Request $request, EntityManagerInterface $entityManager)
{
... lines 19 - 21
// use real logic here to save this to the database
if ($direction === 'up') {
$logger->info('Voting up!');
$answer->setVotes($answer->getVotes() + 1);
$currentVoteCount = rand(7, 100);
} else {
$logger->info('Voting down!');
$answer->setVotes($answer->getVotes() - 1);
}
... lines 31 - 34
}
}

If you want to create fancier methods inside Answer so that you can say things like $answer->upVote(), you totally should. We did that in the Question entity in the last tutorial.

At the bottom, return the real vote count: $answer->getVotes(). The only thing left to do now is save the new vote count to the database. To do that, we need the entity manager. Autowire that as a new argument - EntityManagerInterface $entityManager - and, before the return, call $entityManager->flush().

... lines 1 - 11
class AnswerController extends AbstractController
{
... lines 14 - 16
public function answerVote(Answer $answer, LoggerInterface $logger, Request $request, EntityManagerInterface $entityManager)
{
... lines 19 - 21
// use real logic here to save this to the database
if ($direction === 'up') {
$logger->info('Voting up!');
$answer->setVotes($answer->getVotes() + 1);
$currentVoteCount = rand(7, 100);
} else {
$logger->info('Voting down!');
$answer->setVotes($answer->getVotes() - 1);
}
$entityManager->flush();
return $this->json(['votes' => $answer->getVotes()]);
}
}

Ok team! Test drive time! Refresh. Everything still looks good so... let's vote! Yes! That made a successful Ajax call and the vote increased by 1. More importantly, when we refresh... the new vote count stays! It did save to the database!

Next: we've already learned that any one relationship can have two sides, like the Question is a OneToMany to Answer... but also Answer is ManyToOne to Question. It turns out, in Doctrine, each side is given a special name and has an important distinction.

Leave a comment!

30
Login or Register to join the conversation
t5810 Avatar
t5810 Avatar t5810 | posted 3 months ago | edited

Hi

I downloaded the code, and I followed all steps from the read me. I am coding along as I am watching the course.

My problem was that no matter which button I clicked, up or down, vote has always been up.

If you have the same problem, and you just want to keep watching the course, here is my PHP function that solves the issue. Not the nicest code in the world, but, it works....

    /**
     * @Route("/answers/{id}/vote", methods="POST", name="answer_vote")
     */
    public function answerVote(Answer $answer, LoggerInterface $logger, Request $request, EntityManagerInterface $entityManager)
    {
        $data = json_decode($request->getContent(), true);
        // At this point, $data['direction'] is always empty, so up is always assigned
        //$direction = $data['direction'] ?? 'up';

        // Here I re-format the $direction variable, so we can use it bellow
        $direction = json_decode($data['data'],true);
        $direction = $direction['direction'];

        if ($direction === 'up') {
            $logger->info('Voting up!');
            $answer->setVotes($answer->getVotes() + 1);
            $currentVoteCount = rand(7, 100);
        } else {
            $logger->info('Voting down!');
            $answer->setVotes($answer->getVotes() - 1);
        }

        $entityManager->flush();

        return $this->json(['votes' => $answer->getVotes()]);
    }

Cheers...

1 Reply
Victor Avatar Victor | SFCASTS | t5810 | posted 3 months ago | edited

Hey @t5810 ,

Thank you for sharing this tip with others!

Cheers!

Reply
Maciej-J Avatar
Maciej-J Avatar Maciej-J | posted 8 months ago

Okay this is bad I am omitting this tutorial completely you jumped with Stimulus out of nowhere, the answer-vote_controller.js doesn't work there is a bunch of changes to the code in comparison to the last video that you didn't mention. You need to fix this cause I see in other comments as well that this is causing many problems for ppl.

1 Reply
Szymon Avatar

Cześć Maciej :)
Did you solve your problem? I stuck now with similar issues.
I installed axios and stimulus, put answer-vote_controller.js file but voting for commet still doesn't work.
I am using PHP8 and Symfony6.

Pzdr

Reply

Hey Szymon,

Did you download the course code and start from start/ folder following the instructions in README?

Cheers!

Reply
Szymon Avatar

Hey Victor,
I have my code since first free course Symfony 5 and I code along with videos.
I restarted server, cleared cache in Chrome and it started working :)

Reply

Hey Szymon,

Glad to hear it works for you now! Btw, we do recommend our users to download the course code for each tutorial - that's because we add some minor changes and do updates to make the learning smoother. But if you want, you can follow a course base on the code from the past tutorials, but keep in mind some possible changes.

Cheers!

Reply

Hey Maciej,

We're sorry to hear you had some problems with following this tutorial! We would definitely be glad to help you to figure out the problem so that you could continue it! Could you tell us more about what exactly did you do and what error do you see? Also, the most important question is probably: did you download the course code and started following this tutorial from the start/ directory of it? Or did you start following it on a fresher Symfony version?

Cheers!

Reply
Maciej-J Avatar

Yeah, I tried both versions of your code from start and from the finish none worked. I ended up writing my own js logic and changing the answerVote method and made it work but it took me some time so please make sure stuff works in this tutorial.

Reply

Hey Maciej-J!

Ah, that's definitely not the experience we want! And yes, if there's something wrong, we definitely need to fix it!

I've just done the following to debug the issue:

A) Downloaded the course code from this page, unzipped, and moved into the finish directory (using the finished code is a bit easier to test)

B) Opened README.md and followed the setup instructions (composer install, database setup, web server, etc).

After doing this, if I visit a specific Question page and up/down vote a comment it does seem to work. Note that, if you follow only the required README.md steps like I did, then your site is using pre-built JS/CSS files that come with the code (Webpack Encore isn't running and doesn't need to be running).

So, I wanted to check with actually building the assets as well. I did that with yarn install and yarn watch. After doing that, the voting/JavaScript still seems to work: clicking up/down triggers the clickVote method on assets/controllers/answer-vote_controller.js and the AJAX call triggers just fine.

Could you tell me more about what problems you hit? I'd love to fix something if we need to - but I can't find the issue yet :).

Cheers!

Reply
Stanislaw Avatar
Stanislaw Avatar Stanislaw | posted 27 days ago | edited

Unfortunatelly the votes just doesn't work for me. I downloaded course code so it's exactly the same. It seems when i press the button to upvote or downvote the js code doesn't do anything i tried to check if it even gets called by console logging and nothing. Edit: i switched browser and it works! Buut votes only count up.

Reply

Hey Stanislaw,

Sounds like a cache problem really... Could you try to force refresh the page? For Chrome, you need to open the developer toolbar and click and hold the page reload button for a few seconds, then choose "Empty cache and hard reload" from the dropdown list.

Also, openning the page in Incognito mode may help too.

Cheers!

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

Hi @there , I see quite some questions on this one. Just spent a few hours trying to get this working by updating the original code. Don't really want to use stimulus yet. I have the javascript creating the correct route using a data field in the twig 'data-answer_id="{{ answer.id}}". However, even though I see the correct route in dev tools in the browser I always get a 500 response. My code in the suddenly appeared AnswerController.php is the same as yours. Unfortunately I get an exception caused by a warning:
'Warning: Use of undefined constant data - assumed 'data' (this will throw an Error in a future version of PHP)'.
Well it didn't need to throw an error in the future cause it killed the application! It was the line '$direction = data['direction'] ?? 'up';' that broke it. Any suggestions?

Reply

Hey Gary,

Is that the PHP code? If so, you probably mean "$direction = $data['direction'] ?? 'up';" ? It seems like you missed a dollar sign for the $data var :)

I hope this helps!

Cheers!

Reply
gazzatav Avatar
gazzatav Avatar gazzatav | Victor | posted 1 year ago | edited

victor , hi and thanks, yes that stops the 500 error/reason. For some reason I think I understand but don't know how to stop all the votes are changed to the same value. I'm using a modified version of the older Javascript without stimulus because I want to understand what I'm using. It also looks like yarn is not necessarily behaving correctly, I just don't know what's going on anymore. In public/build and assets I have:

$(document).ready(function(){
    var $container = $('.js-vote-arrows');
    console.log("This is debug", $container);
    $container.find('a').on('click', function(e){
	e.preventDefault();
	var $link = $(e.currentTarget);
	console.log("On 20220713 Current target", e.currentTarget);
	console.log("On 20220713 id", $link.data('answer_id'));

	$.ajax({
	    url: '/answers/'+ $link.data('answer_id') +'/vote/'+ $link.data('direction'),
	    method: 'POST'

	}).then(function(data){
	    console.log(data);
	    $link('.js-vote-total').text(data.votes);
	});
    });
                                                                                                                                                                                                                                                                             notice the date in the call to console.log.  In the console in the browser I have:
$(document).ready(function(){
    var $container = $('.js-vote-arrows');
    console.log("This is debug", $container);
    $container.find('a').on('click', function(e){
	e.preventDefault();
	var $link = $(e.currentTarget);
	console.log("Current target", e.currentTarget);
	console.log("id", $link.data('answer_id'));

	$.ajax({
	    url: '/answers/'+ $link.data('answer_id') +'/vote/'+ $link.data('direction'),
	    method: 'POST'

	}).then(function(data) {
	    console.log(data);
	    $container.find('.js-vote-total').text(data.votes);
	});
    });

I have restarted yarn, done cache:clear, assets:install, refreshed the page many times and the browser cache. Any ideas?

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

Figured it out:


$($link).parent().find('.js-vote-total').text(data.votes) sets the vote on the correct element.
Reply

Hey Gary,

I'm glad to hear you were able to figured it out yourself, well done!

Cheers!

Reply
Marco Avatar

You completely lost me in this chapter when it comes to the votes for the answers. There is quite some information missing, which you did when you refactored the voting off-screen. It's not just the JS part, it's also the AnswerController. I've ran into various issues here, where for the last ~60 videos it was super easy to follow the videos and code along, without downloading any pre-existing code. I highly suggest you update this chapter video to include the missing information.
Thank you very much and cheers!

Reply
Bard R. Avatar
Bard R. Avatar Bard R. | posted 1 year ago

You lost me on the "saving answer votes" section of this lesson. I have gone through all the courses, coding along.. but when I got here I saw you have implemented "stimulus_controller", but going back to the courses I do not see this implementation explained anywhere. What you did in the first course was to add a questionVote method to the QuestionController and up/downvote the question, but its using plain form POST action, not stimulus. Did I miss something?

Reply

Hey Bard R.!

Ah, sorry about confusing you - that was my fault! Before this tutorial (so between the Doctrine tutorial and this Doctrine relations tutorial), I refactored the JavaScript to use Stimulus. The JavaScript - whether it's written in Stimulus or not - is not a topic that I wanted to go into here. We DID need to write a tiny bit of code in Twig (stimulus_controller) to activate that JavaScript... which is what we did here. And yes, you're right, in the original Doctrine tutorial, I had a plain form POST for voting (though I "hijacked" that with JavaScript and submitted it via Ajax to be fancy).

So... this is a long way of saying that I refactored the JavaScript behind the scenes... but my hope was that this didn't cause problems, as the JavaScript itself isn't an important piece. But if you have any questions about it or how it works, I'd be happy to elaborate.

Cheers!

1 Reply
MattHB Avatar

a tiny problem is that the code in the download finish directory is still the un-refactored code, which adds to the confusion quite a bit as it doesnt match the docs or video.

Reply

Hey MattHB!

Hmm, the finish directory still has the un-refactored code? I just downloaded it (specifically, the course download from this course), and I see the Stimulus controller in assets/ as well as stimulus_controller() inside of the template. Do you see something different? Or are you referring to some other bit of code that's still un-refactored in the final code? We definitely want to get this right :). The "final" code is built automatically from the real code we use in the video.

Cheers!

Reply
MattHB Avatar

you were right! (as usual :D ).. It was pilot error.

Reply
MattHB Avatar

oh weird!!! I could easily be me having a brain-f@art..

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

Thanks for the awesome videos, I love your courses!

Upvoting works fine (probably because it's the default...), but clicking down also increases the vote count (probably because it's the default...).
Is there something wrong with my configuration/browser/head, or is there something missing in the code?

The problem to me seems to be that the POST parameters is partly escaped {"data":"{\"direction\":\"down\"}"}, which results to "data" => "{"direction":"down"}" when json_decoding in AnswerController (array with json still in it, not pure array)

Adding second json_decode $data = json_decode($data['data'], true); does work, but it cannot be proper solution?

Another working solution is modifying the answer-vote_controller.js:
`
axios.post(this.urlValue, {

    direction: button.value

})
.then((response) => {

this.voteTotalTarget.innerHTML = response.data.votes;

})
;
`

That is to remove 'data' key and json.stringify, and send post request like in <a href="https://axios-http.com/docs/post_example&quot;&gt;axios documentation</a>
But would this break something else?

Reply

Hey MikkoP

I believe Axios changed and it's now automatically encoding your data. So, if I'm correct, you don't need to do JSON.stringify anymore

That is to remove 'data' key and json.stringify, and send post request like in axios documentation
But would this break something else?
I don't think so, it just seems to me an another way to send your POST parameters

Cheers!

1 Reply

Yes, I have in my code like this:
Because the direction always was undefined.

    $data = json_decode($request->getContent(), true);
    $direction = json_decode($data['data'], true)['direction'];
Reply
Ek24 Avatar

$data = json_decode($request->toArray()['data'],true);

if ($data['direction'] === 'up') {
$answer->setVotes($answer->getVotes() + 1);
} else {
$answer->setVotes($answer->getVotes() - 1);
}

Should work. urgs .. ugly code.

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Ek24 | posted 1 year ago | edited

Hey Ek24

Or, you can implement two routes, one for each direction (up/down). Cheers!

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