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 the 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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.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.9.0
        "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.7.3
        "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.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}