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

Twig Block Tricks

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

This tutorial is all about Doctrine relations... plus a few other Easter eggs along the way. And one big topic we need to talk about is how to create queries that join across these relationships. To do that properly, we're going to build a comment admin section. Well, for now, we're just going to start building it.

Since this will be a new section on the site, let's create a new controller class! And because I'm feeling especially lazy, find your terminal and run:

php bin/console make:controller

Call it CommentAdminController. This creates a new class and one bonus template file. Go check it out!

... lines 1 - 2
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class CommentAdminController extends Controller
{
/**
* @Route("/comment/admin", name="comment_admin")
*/
public function index()
{
return $this->render('comment_admin/index.html.twig', [
'controller_name' => 'CommentAdminController',
]);
}
}

Ok, nice start! Hmm, but let's change that URL to /admin/comment:

... lines 1 - 7
class CommentAdminController extends Controller
{
/**
* @Route("/admin/comment", name="comment_admin")
*/
public function index()
{
... lines 15 - 17
}
}

Let's see what we have so far. Open a new browser tab and go to http://localhost:8000/admin/comment. Awesome! The template even tells us where the source code lives!

Building the Comment Admin Template

Let's open that template and get to work!

{% extends 'base.html.twig' %}
{% block title %}Hello {{ controller_name }}!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code><a href="{{ 'src/Controller/CommentAdminController.php'|file_link(0) }}">src/Controller/CommentAdminController.php</a></code></li>
<li>Your template at <code><a href="{{ 'templates/comment_admin/index.html.twig'|file_link(0) }}">templates/comment_admin/index.html.twig</a></code></li>
</ul>
</div>
{% endblock %}

This already overrides the title block, which is cool! Change it to say "Manage Comments". Then, delete all the body code:

{% extends 'base.html.twig' %}
{% block title %}Manage Comments{% endblock %}
{% block body %}
... lines 6 - 19
{% endblock %}

To make the page look nice, open show.html.twig: we need to steal some markup from this. Copy the first 6 divs. Back in index.html.twig, paste, close each of those 6 divs and... back in the middle, add Manage Comments:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Try that again in your browser: refresh. Ok! Those 6 divs give us this white box that you also see on the article show page.

Creating a Sub-Layout

Hmm. If you think about it, it's probably going to be really common for us to want a page where we have some nice margin and a white box. The homepage doesn't need this, but, I bet a lot of internal pages will.

So, needing to duplicate these six divs on each page is pretty lame. Fortunately, Twig comes to the rescue! Go Twig! We can isolate this markup into a new base layout.

In the templates/ directory, create a new file: content_base.html.twig. What's cool is that, we can extend the normal base.html.twig and then just add the extra markup we need:

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

To do that, override block body just like we would in a normal template. Then, steal the first four divs: these are the divs that really give us the structure. Paste them here, and type a ton of closing div tags:

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
... line 8
</div>
</div>
</div>
</div>
{% endblock %}

Next, and here's the key, in the middle, which is where we want the content to go, create a new block called content_body and {% endblock %}:

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
{% block content_body %}{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

I just invented that name.

And, that's it! Let's go use it! In index.html.twig, change the extends to content_base.html.twig:

{% extends 'content_base.html.twig' %}
... lines 2 - 12

Now, we do not want to override the block body. Nope, we want to override content_body. Thanks to this, we should get the normal base layout plus the extra markup from content_base.html.twig.

Remove the 4 divs, their closing tags, then clean things up a bit:

{% extends 'content_base.html.twig' %}
{% block title %}Manage Comments{% endblock %}
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
</div>
</div>
{% endblock %}

Ok, try it! Go back to the admin tab and refresh. Yay! A nice layout with no work. Repeat this in show.html.twig: extend content_base.html.twig, change the block to content_body, and remove the 4 divs on top, the 4 closing tags on the bottom and... un-indent a few times so this looks decent:

{% extends 'content_base.html.twig' %}
{% block title %}Read: {{ article.title }}{% endblock %}
{% block content_body %}
<div class="row">
<div class="col-sm-12">
... lines 8 - 20
</div>
</div>
... lines 23 - 34
<div class="row">
<div class="col-sm-12">
... lines 37 - 70
</div>
</div>
{% endblock %}
... lines 75 - 81

And unless we forgot something... nope! It still looks perfect! We now have a super easy way to create new pages.

Adding a Custom Class to One Template

Except... there's one small design change I want to make... but I want to make it to the comment admin section only. Our project already has a public/css/styles.css file. On your browser, "Inspect Element" on the white box. One of the CSS classes in styles.css is show-article-container-border-green.

If you add this, you get a nice green border on top! And according to our hard-working and very particular design team, they want this on the "Manage Comments" page, but they do not want it on the article page. Apparently some of our alien readers associate the color green with fictional, untrustworthy content. Well, we can't have that!

But, dang! This ruins everything! The class needs to live on the show-article-container div... but that lives inside content_base.html.twig. How can we change this for only one of the children templates?

The answer is... drumroll... blocks! Blocks are almost always the answer when you need to do cool things with Twig inheritance.

In content_base.html.twig, surround all of the classes with a new block: call it content_class. After the classes, use endblock:

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="{% block content_class %}show-article-container p-3 mt-4{% endblock %}">
... line 8
</div>
</div>
</div>
</div>
{% endblock %}

This defines a new block that has default content. If nobody overrides the block, it will have all of these classes. But, in the comments template, we can override this: {% block content_class %}. But, we don't really want to fully replace it: we want to add to the block's content. No problem: use {{ parent() }} to print the existing classes, then show-article-container-border-green with {% endblock %}:

{% extends 'content_base.html.twig' %}
... lines 2 - 4
{% block content_class %}{{ parent() }} show-article-container-border-green{% endblock %}
... lines 6 - 14

I love it! To make sure I'm not lying: find your browser and refresh the manage comments page. Looks great! But the article show page... nope! No green border: our alien readers will continue to trust us.

Next, let's finish our comment admin page by listing them in a table. And, we'll install a cool library called "Twig Extension".

Leave a comment!

3
Login or Register to join the conversation

Loving these tutorials guys. In case it's useful to anyone else, the styles.css you pickup from the course code in the `Stellar Development with Symfony 4` tutorial, is not exactly the same as the one here.

For example I was missing the styling for `.show-article-container.show-article-container-border-green`, which confused me for a second. The green bar described at the end of this video worked exactly as expected once I added this into my styles.css.

1 Reply

.show-article-container.show-article-container-border-green {
    border-top: 3px solid green;
    border-radius: 3px;
}

the missing css in styles.css, if you are following all the tutorials from the beginning with the same code base

2 Reply

Hey JP,

Thanks for this tip!

Cheers!

1 Reply
Cat in space

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

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.7.2
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "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/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0", // v4.0.14
        "twig/extensions": "^1.5" // v1.5.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}
userVoice