Let Twig Do The Hard Work


In the previous video we created (technically: generated) our first Controller class, along with a single controller method which rendered and returned our index.html.twig template.

Keeping things simple, we put everything inside the templates/welcome/index.html.twig template. In doing so, we created a nice looking web page for the root of our site (/), but when visiting our other route (/hello), we saw a basic, unstyled page.

What we don't want to have to do is copy / paste all of the layout and navbar from one template to another, every time we make a new template, or update an existing one. This would be a bad time.

Instead, what we're going to do is to extract out these common parts of our layout, and then create blocks that we can fill in on a per page basis. It's much easier to see in practice, so let's dive right in.

All About The Base

One thing that I didn't mention in the previous video, but which the symfony/website-skeleton gives us for free is the templates/base.html.twig.

This is a really simple template:

<!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 hard to miss all these block tags.

A block allows us to define areas that can be changed.

Taking the block title as an example, we likely want to set a unique page title tag for each of the pages on our site. We can do this by defining a new {% block title %}...{% endblock %} tag in any template that extends this base template.

Also notice that the block title above comes with text already set inside the block tags. If no block title tag is defined in a child template then the default value, Welcome! will be used.

You can read more about this here.

Let's extract out the common HTML from our templates/welcome/index.html.twig template and move this into the templates/base.html.twig template file:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>{% block title %}Let's Explore Symfony 4{% endblock %}</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"
          integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
          crossorigin="anonymous">
    <link rel="stylesheet" href="{{ asset('css/custom-style.css') }}" />
    {% block stylesheets %}{% endblock %}
</head>

<body>

<header>
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container">

            <a class="navbar-brand" href="{{ path('welcome') }}">Home</a>

            <div class="collapse navbar-collapse">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('hello_page') }}">Hello Page</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</header>

<main role="main" class="container main">
    {% block body %}{% endblock %}
</main>

{% block javascripts %}{% endblock %}
</body>
</html>

It turns out most of our template was generic.

We will be able to add the h1 and "lorum ipsum" text back in via the block body tag from our welcome/index.html.twig template.

Right now though, if we refresh the page in our web browser, nothing has changed.

Symfony isn't a mind reader. Not yet anyway. Maybe in Symfony 5, when they implement advanced T-1000 series terminator logic.

We must tell our welcome/index.html.twig to extends the base.html.twig template.

What this also means is we need to update the welcome/index.html.twig template to remove all the stuff we just extracted.

{% extends 'base.html.twig' %}

{% block body %}
    <div>
        <h1>Let's Explore Symfony 4</h1>
        <p class="lead">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras rutrum sapien mauris, venenatis
            facilisis neque tincidunt vel. Maecenas vel felis vel turpis scelerisque eleifend. Fusce nec purus egestas,
            efficitur nisi ac, ultrices nulla. Pellentesque eu mollis tortor, in mollis nisl. Maecenas rhoncus quam non
            lacinia mollis.</p>
    </div>
{% endblock %}

Everything inside the block body tag is now displayed inside the defined block body area in the template we extends.

If you misspell the exact block name then your text simply won't show up. The rest of the template will still render, and any correctly named blocks will show up. This is an easy mistake to make, and can be confusing to track down. This might not sound so problematic given that we only have four blocks currently, but on larger site structures, it is more easily done.

A Better Hello Page

All of the above would be a lot of work if all we had was a website with one page.

Fortunately, we have two.

And now that we've done all the 'hard work' to setup our site's layout, we can take full advantage of this by changing the templates/hello_page.html.twig and immediately getting the nice styles and the navbar, and a default page title, and so on:

{% extends 'base.html.twig' %}

{% block body %}
    Hello, CodeReviewVideos!
{% endblock %}

As easy as that, we have both pages under one centrally managed layout.

We can give this hello_page a custom title by changing the block title:

{% extends 'base.html.twig' %}

{% block title %}Some custom title{% endblock title %}

{% block body %}
    Hello, CodeReviewVideos!
{% endblock %}

And upon page refresh, we see our new page title.

Notice how I've used endblock title, but for the body block I just used endblock?

Both syntaxes are valid. Typically I prefer just the endblock, but giving the specific block name in the endblock closing tag can be useful to match up tags on more involved templates.

The order of the blocks doesn't matter.

{% block body %}
    Hello, CodeReviewVideos!
{% endblock %}

{% block title %}Some custom title{% endblock title %}

{% extends 'base.html.twig' %}

Even though everything is "upside down", your page will still render properly. My preference is to keep the tags in the order they appear in the parent, for simplicity.

What's app?

As a heads up: you can skip this entire section and jump to the next video if you aren't interested in upping your Symfony geek skills. What we learn here is good to know, but not essential.

Still here? Cool. Let's make our "Hello Page" a bit more interesting.

Rather than hardcoding to "Hello, CodeReviewVideos!", wouldn't it be nicer to pass in a value at run time?

Let's say instead of simply accessing http://127.0.0.1:8000/hello, we could instead access http://127.0.0.1:8000/hello?name=Chris and output "Hello, Chris!".

Given that we aren't using a custom Controller class, or controller method, it seems like this might be a tough one to solve.

However, Twig implicitly receives an app variable. And this app variable gives us access to a bunch of interesting things, such as the current user (app.user), the environment e.g. 'prod', 'dev' (app.environment), the current request (app.request) and a couple of others.

This actually isn't immediately obvious. But you can see this for yourself by using another handy Twig function: dump:

{% extends 'base.html.twig' %}

{% block title %}Some custom title{% endblock title %}

{% block body %}

    {{ dump() }}

    Hello, CodeReviewVideos!
{% endblock %}

Now refresh the page and you will see an array representation dumped out to the screen containing all the variables passed in for this render. In our case we have only that implicit app variable, but shortly we will see more here.

This is showing some more advanced stuff than what you may otherwise need, but I do like to "peel back" the curtains and give a glimpse of what's happening, even if you might not fully (or even partially at this point) understand any of it.

As mentioned above, if we can get access to the current request (app.request) then we can get access to the query parameters for this request. Query parameters are those you see in a URL after the question mark.

If we browse to our page on http://127.0.0.1:8000/hello?name=Chris, then expand out the dump output for:

app > requestStack > request > 0 > query > parameters

Then you should be able to see whatever name query parameter you set:

symfony 4 twig dump output

Let's use this in our template.

{% extends 'base.html.twig' %}

{% block title %}Some custom title{% endblock title %}

{% block body %}

    Hello, {{ app.request.query.get('name') }}!

{% endblock %}

And cool, we see "Hello, Chris!" (or whatever you set), all wrapped nicely in our fancy Bootstrap styles.

There's a downside here.

What if we go rogue and send a request to: http://127.0.0.1:8000/hello

Or, we get fat fingered and misspell our query parameter: http://127.0.0.1:8000/hello?nom=bob

Then we see the less attractive: "Hello, !"

Oh my.

Well, fortunately Twig still has our backs here.

We can set a default:

{% extends 'base.html.twig' %}

{% block title %}Some custom title{% endblock title %}

{% block body %}

    Hello, {{ app.request.query.get('name') | default('CodeReviewVideos') }}!

{% endblock %}

Now if the query parameter is not set, or misspelled, or whatever, we fall back to a nice default: "Hello, CodeReviewVideos!"

This is good, right?

Well, kind of.

I wouldn't actually do this in the real world.

I'm showing you this because knowing you can do this is useful.

But most of the time, the vast majority of the time in my case, you're much more likely to want / need to pass in variables from your Controller methods. So, let's do that in the very next video.

Code For This Course

Get the code for this course.

Episodes