Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Entity objects in Twig

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

We now have a Question object inside our controller. And at the bottom, we render a template. What we need to do is pass that Question object into the template and use it on the page to print the name and other info.

Remove the dd(), leave the $answers - we'll keep those hardcoded for now because we don't have an Answer entity yet - and get rid of the hardcoded $question, and $questionText.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 70
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager)
{
... lines 73 - 79
if (!$question) {
throw $this->createNotFoundException(sprintf('no question found for slug "%s"', $slug));
}
$answers = [
'Make sure your cat is sitting `purrrfectly` still ?',
'Honestly, I like furry shoes better than MY cat',
'Maybe... try saying the spell backwards?',
];
... lines 89 - 93
}
}

Instead pass a question variable to Twig set to the Question object.

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 70
public function show($slug, MarkdownHelper $markdownHelper, EntityManagerInterface $entityManager)
{
... lines 73 - 79
if (!$question) {
throw $this->createNotFoundException(sprintf('no question found for slug "%s"', $slug));
}
$answers = [
'Make sure your cat is sitting `purrrfectly` still ?',
'Honestly, I like furry shoes better than MY cat',
'Maybe... try saying the spell backwards?',
];
return $this->render('question/show.html.twig', [
'question' => $question,
'answers' => $answers,
]);
}
}

Twig's Smart . Syntax

Let's go find the template: templates/question/show.html.twig. The question variable is no longer a string: it's now an object. So... how do we render an object? Because the Question class has a name property, we can say question.name. It even auto-completes it for me! That doesn't always work in Twig, but it's nice when it does.

... lines 1 - 2
{% block title %}Question: {{ question.name }}{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 27
<div class="col">
<h1 class="q-title-show">{{ question.name }}</h1>
<div class="q-display p-3">
<i class="fa fa-quote-left mr-3"></i>
<p class="d-inline">{{ question.question }}</p>
<p class="pt-4"><strong>--Tisha</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
... lines 41 - 68
</div>
{% endblock %}

Below... here's another one - question.name and questionText is now question.question.

... lines 1 - 2
{% block title %}Question: {{ question.name }}{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 27
<div class="col">
<h1 class="q-title-show">{{ question.name }}</h1>
<div class="q-display p-3">
<i class="fa fa-quote-left mr-3"></i>
<p class="d-inline">{{ question.question }}</p>
<p class="pt-4"><strong>--Tisha</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
... lines 41 - 68
</div>
{% endblock %}

I think that's it! Testing time! Move over, go back to the real question slug and... there it is! We have a real name and real question text. This date is still hard coded, but we'll fix that soon.

Now, some of you might be thinking:

Um... how the heck did that work?

We said question.name... which makes it look like it's reading the name property. But... if you look at the name property inside of the Question entity... it's private! That means we can't access the name property directly. What's going on?

... lines 1 - 10
class Question
{
... lines 13 - 22
private $name;
... lines 24 - 91
}

We're witnessing some Twig magic. In reality, when we say question.name, Twig first does look to see if the name property exists and is public. If it were public, Twig would use it. But since it's not, Twig then tries to call a getName() method. Yep, we write question.name, but, behind the scenes, Twig is smart enough to call getName().

... lines 1 - 10
class Question
{
... lines 13 - 22
private $name;
... lines 24 - 44
public function getName(): ?string
{
return $this->name;
}
... lines 49 - 91
}

I love this: it means you can run around saying question.name in your template and not really worry about whether there's a getter method or not. It's especially friendly to non-PHP frontend devs.

If you wanted to actually call a method - like getName() - that is allowed, but it's usually not necessary.

The one thing that we did lose is that, originally, the question text was being parsed through markdown. We can fix that really easily by using the parse_markdown filter that we created in the last tutorial.

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
... lines 13 - 27
<div class="col">
<h1 class="q-title-show">{{ question.name }}</h1>
<div class="q-display p-3">
<i class="fa fa-quote-left mr-3"></i>
<p class="d-inline">{{ question.question|parse_markdown }}</p>
<p class="pt-4"><strong>--Tisha</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
... lines 41 - 68
</div>
{% endblock %}

Refresh and... it works.

The Doctrine Web Debug Toolbar

You may not have noticed, but near the middle of the web debug toolbar, there's a little database icon that says 1 database query. And we can click the icon to jump into the profiler and... see the exact query! If this page made multiple queries, you would see all of them here.

If you ever want to debug a query directly, click "View runnable query" to get a version that you can copy.

Seeing the Profiler for AJAX Requests

Now, here's a challenge: how could we see the INSERT query that's made when we go to /questions/new? This did just make that query... but because we're not rendering HTML, this doesn't have a web debug toolbar. The same problem happens whenever you make an AJAX call.

So... are we out of luck? Nah - we can use a trick. Go to /_profiler to find a list of the most recent requests we've made. Here's the one we just made to /questions/new. Click the little token string on the right to jump into the full profiler for that request! Go to the "Doctrine" tab and... bam! Cool! It even wraps the INSERT in a transaction.

Remember this trick the next time you want to see database queries, a rendered version of an error, or something else for an AJAX request.

Go back a few times to the question show page. The last piece of question data that's hardcoded is this "asked 10 minutes ago" text. Search for it in the template... there it is, line 18.

Let's make this dynamic... but, not just by printing some boring date like "July 10th at 10:30 EST". Yuck. Let's print a much-friendlier "10 minutes ago" type of message next.

Leave a comment!

14
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 11 months ago

noob question about docker. I set up the database service with mariadb, yadda yadda, turned off the machine, turned it back on today and was quite surprised that the question table we created in the migrations chapter was still there, even though my docker-compose.yaml doesn't say anything about volumes. so let's see -- the state of the database persists in the docker image after you shut down the container? so, you only lose your db data if you destroy the image? If so, that seems reasonable enough!

Reply

Hey davidmintz!

Yup, I think you basically got it! Well, let me clarify one point. When you "turn off" a container, you can do one of two different things:

A) docker-compose stop. This turns off the containers, but does not delete them. It's like turning off your computer.
B) docker-compose down. This turns off the containers *and* removes them. It's like turning off your computer then destroying the hard drive.

So, I believe that it's less about "destroying the image" and more about "destroying the container" (which was built from the image). A minor detail, but for understanding things, it might help :). To further the distinction, you could have multiple containers that are all built from the same image, but they each of their own, separate data (e.g. the hard drive of each was built FROM the image, but is now its own, standalone machine).

Cheers!

1 Reply
davidmintz Avatar

Gee, I thought I was getting it but now I really am confused! (-: I'm kidding -- sort of. I do docker-compose down but it doesn't seem to destroy the "hard drive", i.e., the data in the database. I get the sense that where the docker docs say "remove" in this context, they mean remove from memory, not delete from disk. True?

And it feels as though stop is rather like hitting pause... no wait. I see docker also has its pause command. Never mind -- I'll get it all straight at some future date.

Reply
Ibrahim E. Avatar
Ibrahim E. Avatar Ibrahim E. | posted 1 year ago

Could not show question name and question on the page, instead getting this error "Object of class App\Entity\Question could not be converted to string
"

Reply
Ibrahim E. Avatar

I managed to solve the issue by overriding the __toString method in Question class.

Reply

Hey Ibrahim,

Well done! Yes, that error means that you need to add a __toString() magic method to teach the PHP how to convert your object into a string, or call the method explicitly in a place where you're trying to "print" the object.

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

Weird thing is. I dont get autocomplete and question.name doesnt work. But question.getName() does work!
Is that okay? Or is it weird?

Reply

Hey Farry7

It's not strange, it happens to me from time to time. Symfony plugin is not perfect, I usually trigger a "clear index" (inside PHPStorm settings) action and it solves the problem

Cheers!

Reply
Gediminas N. Avatar
Gediminas N. Avatar Gediminas N. | posted 2 years ago

At 0:55 in my IDE it doesn't autocomplete the records, is there some way to make my PHPStorm like yours?

Reply

Hey Gediminas N.

Do you have Symfony plugin installed and enabled in your PHPStorm?

Cheers!

Reply
Gediminas N. Avatar
Gediminas N. Avatar Gediminas N. | sadikoff | posted 2 years ago | edited

Hey sadikoff ,

Yes

Reply

Wow that's weird. Try to check plugin configuration, fix path inside it. There is old default config, which needs to be adjusted after plugin enabling. On main plugin settings you should check app and web directories they should point to src/ and public/ also translation paths should be adjusted. Also it's good to check Twig, Container and Routing tabs. The best way to get everything work here is to press reset to default button

Cheers! I hope this will help!

Reply

Hi,

2:53 little html issue.
By adding parse_markdown filter, the question goes out of p.d-inline tag :/
New paragraphs are generated by markdown.

Cheers!

Reply

Hey Steven!

Haha, yea, you're 100% right :). I noticed this when I was editing the video... but too late then. So yea, in reality, the p tag around the processed markdown should probably be a div, otherwise you get p tags inside of p tags.

Cheers!

1 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": "^7.4.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.1", // 2.1.1
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.7", // 2.8.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "stof/doctrine-extensions-bundle": "^1.4", // v1.5.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.5.0
    }
}