Back to work! Open ArticleController. As soon as you want to render a template, you need to extend a base class: AbstractController:

29 lines src/Controller/ArticleController.php
... lines 1 - 5
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
... lines 7 - 8
class ArticleController extends AbstractController
{
... lines 11 - 27
}

Obviously, your controller does not need to extend this. But they usually will... because this class gives you shortcut methods! The one we want is return $this->render(). Pass it a template filename: how about article/show.html.twig to be consistent with the controller name. The second argument is an array of variables that you want to pass into your template:

29 lines src/Controller/ArticleController.php
... lines 1 - 8
class ArticleController extends AbstractController
{
... lines 11 - 21
public function show($slug)
{
return $this->render('article/show.html.twig', [
... line 25
]);
}
}

Eventually, we're going to load articles from the database. But... hang on! We're not quite ready yet. So let's fake it 'til we make it! Pass a title variable set to a title-ized version of the slug:

29 lines src/Controller/ArticleController.php
... lines 1 - 8
class ArticleController extends AbstractController
{
... lines 11 - 21
public function show($slug)
{
return $this->render('article/show.html.twig', [
'title' => ucwords(str_replace('-', ' ', $slug)),
]);
}
}

Great! Let's go add that template! Inside templates/, create an article directory then the file: show.html.twig.

Add an h1, then print that title variable: {{ title }}:

16 lines templates/article/show.html.twig
<h1>{{ title }}</h1>
... lines 2 - 16

Twig Basics

If you're new to Twig, welcome! You're going to love it! Twig only has 2 syntaxes. The first is {{ }}. I call this the "say something" tag, because it prints. And just like PHP, you can print anything: a variable, a string or a complex expression.

The second syntax is {% %}. I call this the "do something" tag. It's used whenever you need to, um, do something, instead of printing, like an if statement or for loop. We'll look at the full list of do something tags in a minute.

And... yea, that's it! Well, ok, I totally lied. There is a third syntax: {# #}: comments!

At the bottom of this page, I'll paste some extra hard-coded content to spice things up!

16 lines templates/article/show.html.twig
<h1>{{ title }}</h1>
<div>
<p>
Bacon ipsum dolor amet filet mignon picanha kielbasa jowl hamburger shankle biltong chicken turkey pastrami cupim pork chop. Chicken andouille prosciutto capicola picanha, brisket t-bone. Tri-tip pig pork chop short ribs frankfurter pork ham. Landjaeger meatball meatloaf, kielbasa strip steak leberkas picanha swine chicken pancetta pork loin hamburger pork.
</p>
<p>
Kielbasa pork belly meatball cupim burgdoggen chuck turkey buffalo ground round leberkas cow shank short loin bacon alcatra. Leberkas short loin boudin swine, ham hock bresaola turducken tail pastrami picanha pancetta andouille rump landjaeger bacon. Pastrami swine rump meatball filet mignon turkey alcatra. Picanha filet mignon ground round tongue ham hock ball tip tri-tip, prosciutto leberkas kielbasa short loin short ribs drumstick. Flank pig kielbasa short loin jerky ham hock turducken prosciutto t-bone salami pork jowl.
</p>
<p>
Pastrami short loin pork chop, chicken kielbasa swine turducken jerky short ribs beef. Short ribs alcatra shoulder, flank pork chop shankle t-bone. Tail rump pork chop boudin pig, chicken porchetta. Shank doner biltong, capicola brisket sausage meatloaf beef ribs kevin beef rump ribeye t-bone. Shoulder cupim meatloaf, beef kevin frankfurter picanha bacon. Frankfurter bresaola chuck kevin buffalo strip steak pork loin beef ribs prosciutto picanha shankle. Drumstick prosciutto pancetta beef ribs.
</p>
</div>

Let's go try it! Find your browser and refresh! Boom! We have content!

But check it out: if you view the page source... it's just this content: we don't have any layout or HTML structure yet. But, we will soon!

Looping with for

Go back to your controller. Eventually, users will need to be able to comment on the articles, so they can respectfully debate the article's conclusions based on objective analysis and research. Ya know... no different than any other news commenting section. Ahem.

I'll paste in 3 fake comments. Add a second variable called comments to pass these into the template:

36 lines src/Controller/ArticleController.php
... lines 1 - 8
class ArticleController extends AbstractController
{
... lines 11 - 21
public function show($slug)
{
$comments = [
'I ate a normal rock once. It did NOT taste like bacon!',
'Woohoo! I\'m going on an all-asteroid diet!',
'I like bacon too! Buy some from my site! bakinsomebacon.com',
];
return $this->render('article/show.html.twig', [
... line 31
'comments' => $comments,
]);
}
}

This time, we can't just print that array: we need to loop over it. At the bottom, and an h2 that says "Comments" and then add a ul:

24 lines templates/article/show.html.twig
... lines 1 - 16
<h2>Comments</h2>
<ul>
... lines 20 - 22
</ul>

To loop, we need our first do something tag! Woo! Use {% for comment in comments %}. Most "do" something tags also have a closing tag: {% endfor %}:

24 lines templates/article/show.html.twig
... lines 1 - 16
<h2>Comments</h2>
<ul>
{% for comment in comments %}
... line 21
{% endfor %}
</ul>

Inside the loop, comment represents the individual comment. So, just print it: {{ comment }}:

24 lines templates/article/show.html.twig
... lines 1 - 16
<h2>Comments</h2>
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>

Try it! Brilliant! I mean, it's really ugly... oof. But we'll fix that later.

The Amazing Twig Reference

Go to twig.symfony.com and click on the Documentation link. Scroll down a little until you see a set of columns: the Twig Reference.

This is awesome! See the tags on the left? That is the entire list of possible "do something" tags. Yep, it will always be {% and then one of these: for, if, extends, tractorbeam. And honestly, you're only going to use about 5 of these most of the time.

Twig also has functions... which work like every other language - and a cool thing called "tests". Those are a bit unique, but not too difficult, they allow you to say things like if foo is defined or... if space is empty.

The most useful part of this reference is the filter section. Filters are like functions but with a different, way more hipster syntax. Let's try our the |length filter.

Go back to our template. I want to print out the total number of comments. Add a set of parentheses and then say {{ comments|length }}:

24 lines templates/article/show.html.twig
... lines 1 - 16
<h2>Comments ({{ comments|length }})</h2>
... lines 18 - 24

That is a filter: the comments value passes from the left to right, just like a Unix pipe. The length filter counts whatever was passed to it, and we print the result. You can even use multiple filters!

Tip

To unnecessarily confuse your teammates, try using the upper and lower filters over and over again: {{ name|upper|lower|upper|lower|upper }}!

Template Inheritance

Twig has one last killer feature: it's template inheritance system. Because remember! We don't yet have a real HTML page: just the content from the template.

To fix this, at the top of the template, add {% extends 'base.html.twig' %}:

26 lines templates/article/show.html.twig
{% extends 'base.html.twig' %}
... lines 2 - 26

This refers to the base.html.twig file that was added by the recipe:

13 lines templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

It's simple now, but this is our layout file and we'll customize it over time. By extending it, we should at least get this basic HTML structure.

But when we refresh... surprise! An error! And probably one that you'll see at some point!

A template that extends another one cannot include content outside Twig blocks

Huh. Look at the base template again: it's basically an HTML layout plus a bunch of blocks... most of which are empty. When you extend a template, you're telling Twig that you want to put your content inside of that template. The blocks, are the "holes" into which our child template can put content. For example, there's a block called body, and that's really where we want to put our content:

13 lines templates/base.html.twig
<!DOCTYPE html>
<html>
... lines 3 - 7
<body>
{% block body %}{% endblock %}
... line 10
</body>
</html>

To do that, we need to override that block. At the top of the content, add {% block body %}, and at the bottom, {% endblock %}:

28 lines templates/article/show.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<h1>{{ title }}</h1>
... lines 5 - 21
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
{% endblock %}

Now our content should go inside of that block in base.html.twig. Try it! Refresh! Yes! Well, it doesn't look any different, but we do have a proper HTML body.

More about Blocks

You're completely free to customize this template as much as you want: rename the blocks, add more blocks, and, hopefully, make the site look less ugly!

Oh, and most of the time, the blocks are empty. But you can give the block some default content, like with title:

13 lines templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
... line 4
<title>{% block title %}Welcome!{% endblock %}</title>
... line 6
</head>
... lines 8 - 11
</html>

Yep, the browser tab's title is Welcome.

Let's override that! At the top... or really, anywhere, add {% block title %}. Then say Read , print the title variable, and {% endblock %}:

30 lines templates/article/show.html.twig
{% extends 'base.html.twig' %}
{% block title %}Read: {{ title }}{% endblock %}
... lines 4 - 30

Try that! Yes! The page title changes. And... voilà! That's Twig. You're going to love it.

Go Deeper!

Check out another screencast from us to learn more about Twig

Next let's check out one of Symfony's most killer features: the profiler.

Leave a comment!

  • 2018-02-05 weaverryan

    Hey Peter!

    Great question :). This reflects a philosophical difference in Symfony 3 versus 4. Basically:

    Symfony 3: Give the user a bunch of features out-of-the box. But, some features you won't need, and you will still need to install other features

    Symfony 4: Start very micro so that the user has a small and super fast app. We don't know anything about their app (API, web app?), so instead of giving them things they don't need, allow the user to install what they need. BUT, use Symfony Flex to make installation much easier :).

    So, there still *are* many packages that we recommend: like Twig *if* you need a templating engine or Doctrine *if* you need an ORM. To make those packages easier to find and install, we use the "alias" system in Flex.

    As far as *other* templating systems, for example, these are just as easy to install (if a bundle exists in the community) in Symfony 3 or Symfony 4 - not much has changed there.

    I hope this clarifies!

    Cheers!

  • 2018-02-05 Peter

    Hi Ryan,

    my question is why symfony decided that you have to install twig when in symfony 3 it was done automatically. This indicates to me that we are able to use different templates? (blade) for example?