Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Twig ❤️

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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

... 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:

... 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:

... 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 }}:

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

<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:

... 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:

... 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 %}:

... 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 }}:

... 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 }}:

... 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' %}:

{% extends 'base.html.twig' %}
... lines 2 - 26

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

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

<!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 %}:

{% 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:

<!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 %}:

{% 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!

17
Login or Register to join the conversation
Gaetano S. Avatar
Gaetano S. Avatar Gaetano S. | posted 3 years ago | edited

Hello guys,

Is it possible using the href attribute in the if statement of twig? With an example is more clear:



&lt;div class="Tabs-nav is-first"&gt;
      &lt;button class="Tabs-nav-toggle" type="button"&gt;&lt;/button&gt;
      &lt;a class="Tabs-nav-btn is-active" href="#mixed"&gt;BlaBlaBla&lt;/a&gt;
      &lt;a class="Tabs-nav-btn" href="#tweet"&gt;Tweet&lt;/a&gt;
      &lt;a class="Tabs-nav-btn" href="#linkedin"&gt;LinkedIn&lt;/a&gt;
&lt;/div&gt;
&lt;div class="Tabs-content is-active u-desktopOnly" id="mixed"&gt;
      &lt;div class="Grid Grid--tweets"&gt;
            &lt;div class="Grid-inner"&gt;
                  &lt;div class="Grid-wrap"&gt;
&lt;b&gt;pseudo-code {{ if href === '#mixed'}}&lt;/b&gt;
                      {% for item in mixed %}
                          &lt;div class="Grid-item"&gt;

Thanks for your help

Reply

Hi Gaetano S.!

Probably :). But you would need the "href" to be set to a Twig variable. What exactly are you trying to accomplish? Do you want to loop over some variable and create multiple "Tabs-Content" elements... but one of them is slightly different? Let me know - your example helps... but I still can't quite see what you want to do.

Cheers!

Reply
Gaetano S. Avatar
Gaetano S. Avatar Gaetano S. | weaverryan | posted 3 years ago | edited

Hi and thanks for your time,
I would like to show a mix of items tweet and linkedin if href is mixed, only item tweet if href is tweet and only item linkedin if href is linkedin.
Something like this:
`
<div class="Grid-inner">

  <div class="Grid-wrap">

{ if href === '#mixed'}

      {% for item in mixed %}

...
do something
...
{%endfor%}

      {% elseif href === '#tweet' %}

{% for tweet in tweets%}

...
do something

...
{% endfor %}

{%else href === '#linkedin'%}
{% for post in linkedin%}
...
do something
...
{% endfor%}
{% end if %}

  </div>

</div>
`
The html code inside loop for is the same, just the variables (mixed, tweets, linkedin) sent by controller will change to show the good content. An user use a button to toggle between them.

Reply
Gaetano S. Avatar

Hi, finally I found a solution. I changed the html and used the right css. But I would like to know also a different solution by setting a Twig variable. This way I could improve the duplicated code. Thanks again

Reply

Hey Gaetano S.!

Ah, I think I understand! You literally want to be able to read the "#anchor" off of the URL and use that in Twig to do different things based on this part of the URL, right? If so, that part of the URL (e.g. https://yourdomain.com/page#anchor) is not available in PHP at all - it's not actually sent to your server (your server just sees https://yourdomain.com/page). So, this isn't possible to access in PHP or Twig.

However, I think what you are really after is a way to have these 3 sections - mixed, tweets & linkedin - in your Twig template without duplicating the HTML code around them (because all 3 sections are basically identical). I would probably do this:

A) Extract the HTML into a macro - something like this


{% macro tab_content(html) %}
&lt;div class="Grid-inner"&gt;
    ... other markup

    .. finally print the HTML passed to your
    {{ html|raw }}
&lt;/div&gt;
{% endmacro %}

B) In your template, use the special "set" syntax to set each content - for mixed, tweets & linkedin - to a variable. Then pass that variable to the macro:


{% set mixedHtml %}
    {% for item in mixed %}
        ...
    {% endfor %}
{% endset %}

{% set tweetsHtml %}
    {% for tweet in tweets %}
        ...
    {% endfor %}
{% endset %}

{{ _self.tab_content(mixedHtml) }}
...
{{ _self.tab_content(tweetsHtml) }}

I'm using the _self. because I'm assuming you will put the macro inside the same template - you can also put it in a different template.

References: https://twig.symfony.com/doc/3.x/tags/macro.html and https://twig.symfony.com/doc/3.x/tags/set.html

Let me know if that helps!

Cheers!

Reply
Gaetano S. Avatar

Hi, this is a good idea. Just to be a little bit more clear (sorry for my bad explanation of use case), I don't use the "#anchor" of the URL. It's just a button:
https://drive.google.com/op...
What you see is a part of the home page. In the first message I wrote the code to manage this button. There is also the js to add and remove classes to move it (up and down when you click). The '#anchor' is to call the right div with the class 'Tabs-content'.
Thanks again for your help
P.S. I hope is more clear :)

Reply

Hey Gaetano S.!

Ah! It's making more sense now :). Ok, suppose the page loads to "Bla Bla Bla" originally. Now I click on "Tweet". This adds the #tweet to the URL and maybe you even have some JavaScript to change the "tab" to the Tweet tab. When the URL changes to have #tweet on it, there is no full page refresh (sorry, this may already be obvious to you - just saying it in case) and so Symfony/Twig doesn't have any opportunity to render the Tweet content. What you need to do is render all 3 of the tab contents on initial page load (then just switch the tabs with JavaScript). The tricky part (and I think this was maybe your real question) is how to render all 3 tab content areas without duplicating all the markup 3 times. For that, I'd try the macro thing :).

Let me know if that helps!

Cheers!

Reply
Gaetano S. Avatar

Ok, I will try to improve my code with Macro. It will be a good challenge but it won't be easy to have the same result. really Thanks for your help and your time.

Reply
Default user avatar
Default user avatar Andrew Mcgregor | posted 4 years ago

hey iv followed the vidios step by step twice not but i keep getting

these two Exceptions

NotFoundHttpException
ResourceNotFoundException

im not sure what i have done wrong

Reply

Hey Andrew,

What URL do you have when you see that NotFoundHttpException? It should be something like "http://localhost:8000/news/some-slug" where "some-slug" might be any string actually.

Btw, could you open this http://localhost:8000/news/some-slug URL, do you still see the error? If so, try to clear the cache with "bin/console cache:clear --env=dev" or instead of "dev" put the environment you actually use and try again to hit that http://localhost:8000/news/some-slug .

Still the same error? Then could you debug the routes registered in your system with "bin/console debug:router", do you see that "/news/{slug}" path in the output?

Cheers!

Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 4 years ago | edited

Hey there

After you did what that error occurred? If it happend after trying to render a Twig's template, probably you forgot to extend from Symfony's Abstract controller. Add this import to your class Symfony\Bundle\FrameworkBundle\Controller\AbstractController

Cheers!

Reply
Suman kumar P. Avatar
Suman kumar P. Avatar Suman kumar P. | posted 4 years ago

Hi,
I am geetting this error
Attempted to call an undefined method named "render" of class "App\Controller\ArticleController".

Reply

Hey @Hunter Baba!

Ah, yes! Make sure you make your ArticleController extend AbstractController - that’s where this method comes from :). It’s the first thing we do in this chapter.

Cheers!

Reply
Default user avatar

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?

Reply

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!

4 Reply
GDIBass Avatar

Probably worth noting that you can run the following (instead of the
command used in the 1st tutorial) to install basic website plugins,
including twig:

$ composer create-project symfony/website-skeleton my-project

Reply

Hey GDIBass!

Yea, indeed, that's a good tip. I don't *want* people to do that for this tutorial (because I want you to follow along exactly), but actually, yea, the website-skeleton is what we use on the official docs, since it gives you a bit more out of the box :).

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "sensio/framework-extra-bundle": "^5.1", // v5.1.3
        "symfony/asset": "^4.0", // v4.0.3
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.3
        "symfony/web-server-bundle": "^4.0", // v4.0.3
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "sensiolabs/security-checker": "^5.0", // v5.0.3
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.3
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.3
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.3
    }
}
userVoice