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

Adding a Twig Extension + DI Tags

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

Because the $funFact needs to be parsed through markdown, we have to pass it as an independent variable into the template. You know what would be way cooler? If we could just say genus.funFact|markdown. Ok, actually we can do this already: the KnpMarkdownBundle comes with a filter called markdown:

... lines 1 - 4
{% block body %}
<h2 class="genus-name">{{ genus.name }}</h2>
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 15
<dt>Fun Fact:</dt>
<dd>{{ genus.funFact|markdown }}</dd>
... lines 18 - 19
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 25 - 42

Remove the app.php from the URL and refresh. Voilà! This parses the string to markdown. Well, it doesn't look like much - but if you view the HTML, there's a p tag from the process.

Let's make this a little more obvious while we're working. Open up the Genus entity and find getFunFact(). Temporarily hack in some bold markdown code:

... lines 1 - 11
class Genus
{
... lines 14 - 86
public function getFunFact()
{
return '**TEST** '.$this->funFact;
}
... lines 91 - 113
}

Ok, try it again.

Nice! The bold TEST tells us that the markdown filter from KnpMarkdownBundle is working.

Ready for me to complicate things? I always do. The markdown filter uses the markdown.parser service from KnpMarkdownBundle - it does not use our app.markdown_transformer. And this means that it's not using our caching system. Instead, let's create our own Twig filter.

Creating a Twig Extension

To do that, you need a Twig "extension" - that's basically Twig's plugin system. Create a new directory called Twig - and nope, that name is not important. Inside, create a new php class - MarkdownExtension:

... line 1
namespace AppBundle\Twig;
class MarkdownExtension extends \Twig_Extension
{
... lines 7 - 22
}

Remember: Twig is its own, independent library. If you read Twig's documentation about creating a Twig extension, it will tell you to create a class, make it extend \Twig_Extension and then fill in some methods.

Use the "Code"->"Generate" menu - or cmd+n - and select "Implement Methods". The one method you must have is called getName(). It's also the most boring: just make it return any unique string - like app_markdown:

... lines 1 - 4
class MarkdownExtension extends \Twig_Extension
{
... lines 7 - 18
public function getName()
{
return 'app_markdown';
}
}

To add a new filter, go back to the "Code"->"Generate" menu and select "Override Methods". Choose getFilters():

... lines 1 - 4
class MarkdownExtension extends \Twig_Extension
{
public function getFilters()
{
... lines 9 - 11
}
... lines 13 - 22
}

Here, you'll return an array of new filters: each is described by a new \Twig_SimpleFilter object. The first argument will be the filter name - how about markdownify - that sounds fun. Then, point to a function in this class that should be called when that filter is used: parseMarkdown:

... lines 1 - 4
class MarkdownExtension extends \Twig_Extension
{
public function getFilters()
{
return [
new \Twig_SimpleFilter('markdownify', array($this, 'parseMarkdown'))
];
}
... lines 13 - 22
}

Create the new public function parseMarkdown() with a $str argument. For now, just strtoupper() that guy to start:

... lines 1 - 4
class MarkdownExtension extends \Twig_Extension
{
... lines 7 - 13
public function parseMarkdown($str)
{
return strtoupper($str);
}
... lines 18 - 22
}

Cool? Update the Twig template to use this:

... lines 1 - 4
{% block body %}
<h2 class="genus-name">{{ genus.name }}</h2>
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 15
<dt>Fun Fact:</dt>
<dd>{{ genus.funFact|markdownify }}</dd>
... lines 18 - 19
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 25 - 42

Our Twig extension is perfect... but it will not work yet. Refresh. Huge error:

Unknown markdownify filter.

Twig doesn't automatically find and load our extension. Somehow, we need to say:

Hey Symfony? How's it going? Oh, I'm good. Listen, when you load the twig service, can you add my MarkdownExtension to it?

How are we going to do this? With tags.

Leave a comment!

8
Login or Register to join the conversation
Default user avatar
Default user avatar Tin Pham | posted 5 years ago

Hi Ryan,
in my PHPStorm, I can not see the Twig_ExtensionInterface in the implemented methods (Cmd+N menu). Do I need to install any plugin to see it?

Reply
Default user avatar

Starting with 3.2 Symfony deprecated getName() function in Twig_Extension class, now it's enough to just override the getFilters() method. More info here http://symfony.com/doc/curr....

Reply

Woohoo! You're right! That's an awesome improvement.

To clarify, starting in Twig 1.26 (released October 2nd, 2016), you do not need the getName method anymore. So when you go to "Implement Methods", getName will not show up anymore. And that's totally fine - just skip it (i.e. don't add it).

Thanks Vadim for the comment!

Reply

Hey Tin,

No, it should be available out-of-the-box. Do you choose the "Implement methods" option in "Generate" menu? Please, also double check that your `MarkdownExtension` extends `\Twig_Extension` class. Do you use the latest PhpStorm version? Probably you just need to restart PhpStorm - it should help in most cases.

Cheers!

Reply
Default user avatar

Hi Victor, Thank you for your reply but I still didn't have that getName() method from the "Implement methods" in "Generated" menu.
On the other hand, the override methods is fine.
My PhpStorm version is up to date and I did try to restart it.
I decided to ignore this small bug and move on!

Reply

Ah, that's weird. If you add vendor dir to excluded folders - I think it may cause this issue too. But if not, I can suggest you to re-index your project. AFAIK, you need to choose "File" -> "Invalidate Caches". I hope it will help.

Cheers!

Reply
Agnes Avatar

Same problem with PHPStorm 10 on Windows. Invalidating Cache did not work. Strange, but not a show stopper.
EDITED
Oh wait, not strange. I probably have a later version of twig which already implements the getName method so it shows up in overrides instead.

1 Reply
Victor Avatar Victor | SFCASTS | Agnes | posted 5 years ago | edited

Hey Maria,

Ah, it makes sense! Thanks for sharing this with us. Agnes , please, check Maria's answer about your problem. I bet you will find this method in "Override" menu.

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice