The Twig Extensions Library

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

Let's bring this section to life by listing all of the comments in the system. Ah man, with all our tools, this is going to be really easy! First, to query for the comments, add the CommentRepository $repository argument:

... lines 1 - 4
use App\Repository\CommentRepository;
... lines 6 - 8
class CommentAdminController extends Controller
{
/**
* @Route("/admin/comment", name="comment_admin")
*/
public function index(CommentRepository $repository)
{
... lines 16 - 20
}
}

Then, $comments = $repository->, and we could use findAll(), but I'll use findBy() passing this an empty array, then 'createdAt' => 'DESC' so that we get the newest comments on top:

... lines 1 - 8
class CommentAdminController extends Controller
{
/**
* @Route("/admin/comment", name="comment_admin")
*/
public function index(CommentRepository $repository)
{
$comments = $repository->findBy([], ['createdAt' => 'DESC']);
... lines 17 - 20
}
}

Clear out the render() variables: we only need to pass one: comments set to $comments:

... lines 1 - 8
class CommentAdminController extends Controller
{
/**
* @Route("/admin/comment", name="comment_admin")
*/
public function index(CommentRepository $repository)
{
$comments = $repository->findBy([], ['createdAt' => 'DESC']);
return $this->render('comment_admin/index.html.twig', [
'comments' => $comments,
]);
}
}

Perfect! Next, to the template! Below the h1, I'll paste the beginning of a table that has some Bootstrap classes and headers for the article name, author, the comment itself and when it was created:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Article</th>
<th>Author</th>
<th>Comment</th>
<th>Created</th>
</tr>
</thead>
<tbody>
... lines 22 - 39
</tbody>
</table>
</div>
</div>
{% endblock %}

No problem! In the tbody, let's loop: for comment in comments, and {% endfor %}:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Article</th>
<th>Author</th>
<th>Comment</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
... lines 23 - 38
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Add the <tr>, then let's print some data! In the first td, we need the article name. But, to make it more awesome, let's make this a link to the article. Add the a tag with href="", but keep that blank for a moment. Inside, hmm, we have a Comment object, but we want to print the article's title. No problem! We can use our relationship: comment.article - that gets us to the Article object - then .title.

For the href, use the path() function from Twig. Here, we need the name of the route that we want to link to. Open ArticleController. Ah! There it is: name="article_show:

... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 37
/**
* @Route("/news/{slug}", name="article_show")
*/
public function show(Article $article, SlackClient $slack)
{
... lines 43 - 49
}
... lines 51 - 63
}

Close that and, back in the template, use article_show. This route needs a slug parameter so add that, set to comment.article.slug:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Article</th>
<th>Author</th>
<th>Comment</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
<tr>
<td>
<a href="{{ path('article_show', {'slug': comment.article.slug}) }}">
{{ comment.article.title }}
</a>
</td>
... lines 29 - 37
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Dang, those relationships are handy!

Let's keep going! Add another td and print comment.authorName:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
... lines 13 - 20
<tbody>
{% for comment in comments %}
<tr>
<td>
<a href="{{ path('article_show', {'slug': comment.article.slug}) }}">
{{ comment.article.title }}
</a>
</td>
<td>
{{ comment.authorName }}
</td>
... lines 32 - 37
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Give the next td a style="width: 20%" so it doesn't get too big. Then, print comment.content:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
... lines 13 - 20
<tbody>
{% for comment in comments %}
<tr>
<td>
<a href="{{ path('article_show', {'slug': comment.article.slug}) }}">
{{ comment.article.title }}
</a>
</td>
<td>
{{ comment.authorName }}
</td>
<td style="width: 20%;">
{{ comment.content }}
</td>
... lines 35 - 37
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Finally, add a td with comment.createdAt|ago:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
... lines 13 - 20
<tbody>
{% for comment in comments %}
<tr>
<td>
<a href="{{ path('article_show', {'slug': comment.article.slug}) }}">
{{ comment.article.title }}
</a>
</td>
<td>
{{ comment.authorName }}
</td>
<td style="width: 20%;">
{{ comment.content }}
</td>
<td>
{{ comment.createdAt|ago }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Cool! Let's see if we made any mistakes. Find your browser, refresh and... boom! A big, beautiful list of all of the comments on the site. Oh, but eventually on production, this will be a huge number of results. Let's put it on our todo list to add pagination.

The N+1 Query Problem

Hmm, it works, but check out the web debug toolbar: 11 queries. This is that same annoying N+1 problem that we talked about earlier. The first query is the one we expect: SELECT all of the comments from the system. But, as we loop over each comment and fetch data from its related article, an extra query is made to get that data.

Like, this query fetches the data for article id 181, this does the same for article id 186, and so on. We get 11 queries because we have 1 query for the comments and 10 more queries for the 10 related articles.

Hence, the N+1 problem: 10 related object plus the 1 original query. So, the question is, how can we solve this, right? Well, actually, a better question is: should we solve this? Here's the point: you need to be aware of the fact that Doctrine's nice relationship lazy-loading magic makes it easy to accidentally make many queries on a page. But, it is not something that you always need to solve. Why? Well, a lot of times, having 10 extra queries - especially on an admin page - is no big deal! On the other hand, maybe 100 extra queries on your homepage, well, that probably is a problem. As I always like to say, deploy first, then see where you have problems. Using a tool like Blackfire.io makes it very easy to find real issues.

Anyways, we will learn how to fix this in a few minutes. But, ignore it for now.

Installing Twig Extensions

Because... we have a minor, but more immediate problem: some comments will probably be pretty long. So, printing the entire comment will become a problem. What I really want to do is show some sort of preview, maybe the first 30 characters of a comment.

Hmm, can Twig do that? Go to twig.symfony.com and click on the Documentation. Huh, there is actually not a filter or function that can do this! We could easily add one, but instead, search for "Twig extensions" and click on the documentation for some Twig extensions library.

We know that if we need to create a custom Twig function or filter, we create a class called a Twig extension. We did it in an earlier tutorial. But this is something different: this is an open source library called "Twig Extensions". It's simply a collection of pre-made, useful, Twig extension classes. Nice!

For example, one Twig extension - called Text - has a filter called truncate! Bingo! That's exactly what we need. Click on the "Text" extension's documentation, then click the link to install it. Perfect! Copy that composer require line.

Then, find your terminal and, paste!

composer require twig/extensions

Activating the Twig Extension

While we're waiting for this to install, I want to point out something important: we're installing a PHP library, not a Symfony bundle. What's the difference? Well, a PHP library simply contains classes, but does not automatically integrate into your app. Most importantly, this means that while this library will give us some Twig extension PHP classes, it will not register those as services or make our Twig service aware of them. We will need to configure things by hand.

Go back to the terminal and, oh! Let's play a thrilling game of Pong while we wait. Go left side, go left side, go! Boooo!

Anyways, ooh! This installed a recipe! I committed my changes before I started recording. So let's run:

git status

to see what changed. Beyond the normal Composer files and symfony.lock, the recipe created a new file: config/packages/twig_extensions.yaml. Ah, go check it out!

services:
_defaults:
public: false
autowire: true
autoconfigure: true
#Twig\Extensions\ArrayExtension: ~
#Twig\Extensions\DateExtension: ~
#Twig\Extensions\IntlExtension: ~
#Twig\Extensions\TextExtension: ~

Nice! As we just talked about, the library simply gives us the extension classes, but it does not register them as services. So, to make life easier, the Flex recipe for the library gives us the exact configuration we need to finish the job! Here, we can activate the extensions by uncommenting the ones we need:

services:
_defaults:
public: false
autowire: true
autoconfigure: true
... lines 6 - 9
Twig\Extensions\TextExtension: ~

Actually - because knowledge is power! - there are a few things going on. Thanks to the Twig\Extensions\TextExtension: ~ part, that class becomes registered as as service. Remember: each class in the src/ directory is automatically registered as a service. But because this class lives in vendor/, we need to register it by hand. Oh, and the ~ means null: it means we don't need to configure this service in any special way. For example, we don't need to configure any arguments.

Second, thanks to the _defaults section on top, specifically autoconfigure, Symfony notices this is a Twig Extension by its interface, and automatically notifies the Twig service about it, without us needing to do anything.

All of this means that in index.html.twig, we can now immediately add |truncate:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<table class="table table-striped">
... lines 13 - 20
<tbody>
{% for comment in comments %}
<tr>
... lines 24 - 31
<td style="width: 20%;">
{{ comment.content|truncate }}
</td>
... lines 35 - 37
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

In fact, before we even try it, go back to your terminal and run:

php bin/console debug:twig

This nice little tool shows us all of the functions, filters and other goodies that exist in Twig. And, ha! We now have a filter called truncate!

So, try it: find your browser, go back to the Manage Comments page, and refresh! It's perfect! Oh, and don't forget about the other cool stuff this Twig Extensions library has, like Intl for date or number formatting and, actually, Date, which coincidentally has a time_diff filter that works like our ago filter.

Next! Let's add a search form to the comment admin page.

Leave a comment!

  • 2020-02-15 Rob

    Hey Ryan and Vladimir! No more issues; composer require twig/string-extra enabled the use of u.truncate in my twig template. Thanks for your help guys!

  • 2020-02-14 Vladimir Sadicov

    Hey Rob

    Great! Is there any more issues?

    Cheers!

  • 2020-02-14 Rob

    I was able to get truncate working by running: composer require twig/string-extra

    then print the first 100 words with: comment.content|u.truncate(100)

  • 2020-02-12 weaverryan

    Hey Rob!

    Ah, it looks like you're in dependency "heck" :D. So here's the problem - I think you guessed it already. Basically: twig/extensions has *not* be updated to work with Symfony 5. Normally, it IS a common practice for twig/extensions to be updated when a new Symfony version is released. However, this package has been replaced by a few, smaller libraries (which I like much better). Check out the README here: https://github.com/twigphp/...

    Basically, you should now install a new TwigExtraBundle (depending on when you installed Twig, you may already have this). Then, you can immediately try to start using any of the functions or filters from twig extensions. When you do, you'll get a really clear error telling you *exactly* what other package you should install. Here is a screencast talking about this exactly: https://symfonycasts.com/sc...

    Let me know if that works!

    Cheers!

  • 2020-02-12 Rob

    composer require twig/extensions

    below is the

    Using version ^1.5 for twig/extensions
    ./composer.json has been updated
    Loading composer repositories with package information
    Updating dependencies (including require-dev)
    Restricting packages listed in "symfony/symfony" to "5.0.*"
    Your requirements could not be resolved to an installable set of packages.

    Problem 1
    - Conclusion: don't install twig/extensions v1.5.4
    - Conclusion: don't install twig/extensions v1.5.3
    - Conclusion: don't install twig/extensions v1.5.2
    - Conclusion: don't install twig/extensions v1.5.1
    - Conclusion: remove doctrine/doctrine-bundle 2.0.7
    - Conclusion: don't install doctrine/doctrine-bundle 2.0.7
    - twig/twig v2.0.0 conflicts with doctrine/doctrine-bundle[2.0.7].

    then more conflicts up to
    - twig/twig v1.42.5 conflicts with symfony/http-kernel[v5.0.4].
    and
    - Conclusion: don't install twig/twig v3.0.3|install twig/twig v1.27.0|install twig/twig v1.28.0

  • 2020-02-11 Diego Aguiar

    Hey Rob

    That's odd. I believe it should work. Can you show me what errors are you having?

    Cheers!

  • 2020-02-11 Rob

    I'm working on a Symfony 5.0.4 app and installing twig extensions using: composer require twig/extensions fails installation. Is it common pratice that a library like Twig extension get updated to run in the new versions of Symfony?

  • 2020-01-20 Victor Bocharsky

    Hey Ed,

    Thank you for this tip!

    Cheers!

  • 2020-01-19 Ed Barnard

    For Symfony 5, it appears the approach is to use the "u" filter which provides access to the Symfony string component. Examples and composer installation are here: https://twig.symfony.com/do...

  • 2019-11-25 Virgile Sahaguian

    Thanks you a lot Ryan!

    I fixed it, thanks to composer upgrade !

    Cheers :p

  • 2019-11-18 weaverryan

    Hey Virgile Sahaguian!

    Excellent question :). This is a really cool warning that you get telling you that some dependencies in your project are out-of-date and that those out-of-date versions contain security vulnerabilities. On this project, it's no big deal (as long as you're using this code to practice, and not actually deploy as a real site). If this *were* a real application, you would need to upgrade some of your dependencies. Sometimes composer upgrade would be enough. But because (in this case) 4.3 is end-of-life, you might need to upgrade the application from 4.3 to 4.4 - you can see details about how to do that here: https://symfony.com/doc/cur...

    Let me know if you have any questions!

    Cheers!

  • 2019-11-14 Virgile Sahaguian

    Hello,

    When i installed twig extension i got this :

    Executing script cache:clear [OK]
    Executing script assets:install public [OK]
    Executing script security-checker security:check [KO]
    [KO]
    Script security-checker security:check returned with error code 1
    !! Symfony Security Check Report
    !! =============================
    !!
    !! 5 packages have known vulnerabilities.
    !!
    !! symfony/cache (v4.3.6)
    !! ----------------------
    !!
    !! * [CVE-2019-18889][]: Forbid serializing AbstractAdapter and TagAwareAdapter instances
    !!
    !! symfony/http-foundation (v4.3.6)
    !! --------------------------------
    !!
    !! * [CVE-2019-18888][]: Prevent argument injection in a MimeTypeGuesser
    !!
    !! symfony/http-kernel (v4.3.6)
    !! ----------------------------
    !!
    !! * [CVE-2019-18887][]: Use constant time comparison in UriSigner
    !!
    !! symfony/mime (v4.3.6)
    !! ---------------------
    !!
    !! * [CVE-2019-18888][]: Prevent argument injection in a MimeTypeGuesser
    !!
    !! symfony/var-exporter (v4.3.6)
    !! -----------------------------
    !!
    !! * [CVE-2019-11325][]: Fix escaping of strings in VarExporter
    !!
    !! [CVE-2019-18889]: https://symfony.com/cve-201...
    !! [CVE-2019-18888]: https://symfony.com/cve-201...
    !! [CVE-2019-18887]: https://symfony.com/cve-201...
    !! [CVE-2019-18888]: https://symfony.com/cve-201...
    !! [CVE-2019-11325]: https://symfony.com/cve-201...
    !!
    !! Note that this checker can only detect vulnerabilities that are referenced in the SensioLabs security advisories database.
    !! Execute this command regularly to check the newly discovered vulnerabilities.

  • 2019-08-22 Diego Aguiar

    Thanks! And if you don't feel like fixing it yourself, just ping us :)

  • 2019-08-21 Ozornick

    Ok i will try next time

  • 2019-08-21 Diego Aguiar

    NP man. It's very easy to do it. You just have to click on the "Edit on Github" button in this page, then you will see a text box with the script content, there is where you apply the change, then add a message to the commit and click on "Commit changes" button, and that's it :)

  • 2019-08-21 Ozornick

    Guys, I don’t have a lot of bug report practice. Is it a fork to do? It’s easier for me to write a comment here. Sorry

  • 2019-08-21 Diego Aguiar

    Haha, I can tell ;)
    BTW, if you find another typo or error on a script and feel like fixing it. There is a "Edit on Github" button at the right upper corner of the script block that you click, so you can edit the script, and then we can merge the change into the project. If not, pinging us is just perfect :)

    Cheers!

  • 2019-08-21 Ozornick

    I carefully read the courses =)

  • 2019-08-21 Diego Aguiar

    Totally! Thanks for notifying us :)

    Cheers!

  • 2019-08-21 Ozornick
    Thanks to the Twig\Extensions\TwigExtension: ~ part, that class becomes registered as as service.


    Replace Twig\Extensions\TextExtension?

  • 2019-07-12 Coder

    ping pong :D

  • 2018-11-22 Victor Bocharsky

    Hey Dominik,

    Hm, good question! As far as I remember, N+1 problem is solved with addSelect() and query direction is not important, but I'm not 100% sure about it. Btw, you can double check if it's true with Symfony Web Debug Toolbar - go to Doctrine queries and see if you have extra queries for those data you use in addSelect() or no.

    Cheers!

  • 2018-11-20 Dominik Pakosz

    Is creating query in CommentRepository with `->leftJoin(c.article)->addSelect(article) going to solve the N+1 issue? This is from your tutorial `Going Pro with Doctrine`, but actually the situation there was the opposite to the situation down here. In `Going Pro with Doctrine` you were quering owning part of relationship, but here you are quering inverse. That's why I'm asking.

  • 2018-09-20 Victor Bocharsky

    Hey Steffen,

    We're sorry about that! I just double checked our source code for this chapter: https://github.com/knpunive... - it looks good, so that means we have a bug in rendering the chapter on our website. We'll fix it as soon as possible.

    Thank you for reporting this!

    Cheers!

  • 2018-09-19 Steffen Zeidler

    @SymfonyCasts: There is a broken link in the part "Installing Twig Extensions".

  • 2018-05-11 Victor Bocharsky

    Hey Matt,

    Thanks for sharing this with others!

    Cheers!

  • 2018-05-11 Matt Johnson

    FYI for anyone doing this who runs into:

    The intl extension is needed to use intl-based filters.

    You'll have to install the php-intl package. On Ubuntu with PHP7.2 it's:

    $ apt-get install php7.2-intl