KnpMarkdownBundle & Service
Fun fact! Witches & wizards love writing markdown. I have no idea why... but darnit! We're going to give the people what they want! We're going to allow the question text to be written in Markdown. For now, we'll focus on this "show" page.
Open up QuestionController
and find the show()
method:
// ... lines 1 - 9 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 12 - 26 | |
/** | |
* @Route("/questions/{slug}", name="app_question_show") | |
*/ | |
public function show($slug) | |
{ | |
$answers = [ | |
'Make sure your cat is sitting purrrfectly still ?', | |
'Honestly, I like furry shoes better than MY cat', | |
'Maybe... try saying the spell backwards?', | |
]; | |
return $this->render('question/show.html.twig', [ | |
'question' => ucwords(str_replace('-', ' ', $slug)), | |
'answers' => $answers, | |
]); | |
} | |
} |
Let's see, this renders show.html.twig
... open up that template... and find the question text. Here it is:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-12"> | |
// ... line 9 | |
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);"> | |
<div class="q-container-show p-4"> | |
<div class="row"> | |
// ... lines 13 - 15 | |
<div class="col"> | |
// ... line 17 | |
<div class="q-display p-3"> | |
// ... line 19 | |
<p class="d-inline">I've been turned into a cat, any thoughts on how to turn back? While I'm adorable, I don't really care for cat food.</p> | |
// ... line 21 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
// ... lines 29 - 56 | |
</div> | |
{% endblock %} |
Because we don't have a database yet, the question is hardcoded. Let's move this text into our controller, so we can write some code to transform it from Markdown to HTML.
Copy the question text, delete it, and, in the controller, make a new variable: $questionText =
and paste. Pass this to the template as a new questionText
variable:
// ... lines 1 - 9 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 12 - 29 | |
public function show($slug) | |
{ | |
// ... lines 32 - 36 | |
$questionText = 'I\'ve been turned into a cat, any thoughts on how to turn back? While I\'m adorable, I don\'t really care for cat food.'; | |
return $this->render('question/show.html.twig', [ | |
// ... line 40 | |
'questionText' => $questionText, | |
// ... line 42 | |
]); | |
} | |
} |
Back in show.html.twig
, print that: {{ questionText }}
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-12"> | |
// ... line 9 | |
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);"> | |
<div class="q-container-show p-4"> | |
<div class="row"> | |
// ... lines 13 - 15 | |
<div class="col"> | |
// ... line 17 | |
<div class="q-display p-3"> | |
// ... line 19 | |
<p class="d-inline">{{ questionText }}</p> | |
// ... line 21 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
// ... lines 29 - 56 | |
</div> | |
{% endblock %} |
Oh, and to make things a bit more interesting, let's add some markdown formatting - how about **
around "adorable":
// ... lines 1 - 9 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 12 - 29 | |
public function show($slug) | |
{ | |
// ... lines 32 - 36 | |
$questionText = 'I\'ve been turned into a cat, any thoughts on how to turn back? While I\'m **adorable**, I don\'t really care for cat food.'; | |
// ... lines 38 - 43 | |
} | |
} |
Perfect!
If we refresh the page now... no surprise - it literally prints **adorable**
.
Transforming text from Markdown into HTML is clearly "work"... and we know that all work in Symfony is done by a service. And... who knows? Maybe Symfony already has a service that parses markdown. At your terminal, let's find out. Run:
php bin/console debug:autowiring markdown
Installing KnpMarkdownBundle
Nope! And that makes sense: Symfony starts small but makes it super easy to add more stuff. Since I don't want to write a markdown parser by hand - that would be crazy - let's find something that can help! Google for KnpMarkdownBundle
and find its GitHub page. This isn't the only bundle that can parse markdown, but it's a good one. My hope is that it will add a service to our app that can handle all the markdown parsing for us.
Copy the Composer require line, find your terminal and paste:
composer require knplabs/knp-markdown-bundle
This installs and... it configured a recipe! Run:
git status
It updated the files we expect: composer.json
, composer.lock
and symfony.lock
but it also updated config/bundles.php
! Check it out: we have a new line at the bottom that initializes the new bundle:
// ... lines 1 - 2 | |
return [ | |
// ... lines 4 - 11 | |
Knp\Bundle\MarkdownBundle\KnpMarkdownBundle::class => ['all' => true], | |
]; |
Finding the new Service
Ok, so if the main purpose of a bundle is to give us more services... then we probably have at least one new one! Find your terminal and run debug:autowiring markdown
again:
php bin/console debug:autowiring markdown
Yes! There are two services. Well actually, both of these interfaces are a way to get the same service object. See this little blue text - markdown.parser.max
? We'll talk more about this later, but each "service" in Symfony has a unique "id". This service's unique id is apparently markdown.parser.max
and we can get that service by using either type-hint.
It doesn't really matter which one we use, but if you check back on the bundle's documentation... they use MarkdownParserInterface
.
Let's do it! In QuestionController::show()
add a second argument: MarkdownParserInterface $markdownParser
:
// ... lines 1 - 4 | |
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
// ... lines 6 - 10 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 13 - 30 | |
public function show($slug, MarkdownParserInterface $markdownParser) | |
{ | |
// ... lines 33 - 45 | |
} | |
} |
Down below, let's say $parsedQuestionText =
$markdownParser->... I love this: we don't even need to look at documentation to see what methods this object has. Thanks to the type-hint, PhpStorm tells us exactly what's available. Use transformMarkdown($questionText)
. Now, pass this variable into the template:
// ... lines 1 - 10 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 13 - 30 | |
public function show($slug, MarkdownParserInterface $markdownParser) | |
{ | |
// ... lines 33 - 37 | |
$questionText = 'I\'ve been turned into a cat, any thoughts on how to turn back? While I\'m **adorable**, I don\'t really care for cat food.'; | |
$parsedQuestionText = $markdownParser->transformMarkdown($questionText); | |
return $this->render('question/show.html.twig', [ | |
// ... line 42 | |
'questionText' => $parsedQuestionText, | |
// ... line 44 | |
]); | |
} | |
} |
Twig Output Escaping: The "raw" Filter
Love it! Will it work? Who knows? Move over and refresh. It... sorta works! But it's dumping out the HTML tags! The reason... is awesome. If you inspect the HTML... here we go... Twig is using htmlentities
to output escape the text. Twig does that automatically for security: it protects against XSS attacks - that's when users try to enter JavaScript inside a question so that it will render & execute on your site. In this case, we do want to allow HTML because it's coming from our Markdown process. To tell Twig to not escape, we can use a special filter |raw
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-12"> | |
// ... line 9 | |
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);"> | |
<div class="q-container-show p-4"> | |
<div class="row"> | |
// ... lines 13 - 15 | |
<div class="col"> | |
// ... line 17 | |
<div class="q-display p-3"> | |
// ... line 19 | |
<p class="d-inline">{{ questionText|raw }}</p> | |
// ... line 21 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
// ... lines 29 - 56 | |
</div> | |
{% endblock %} |
By the way, in a real app, because the question text will be entered by users we don't trust, we would need to do a bit more work to prevent XSS attacks. I'll mention how in a minute.
Anyways, now when we refresh... it works! It's subtle, but that word is now bold.
The twig:debug Command
By the way, you can of course read the Twig documentation to learn that this raw
filter exists. But Symfony also has a command that will tell you everything Twig can do. At your terminal, run:
php bin/console debug:twig
How cool is that? This shows us the Twig "tests", filters, functions - everything Twig can do in our app. Here's the raw
filter.
The markdown Twig Filter
And... oh! Apparently there's a filter called markdown
! If you go back to the bundle's documentation and search for |markdown
... yeah! So, in addition to the MarkdownParserInterface
service, this bundle also apparently gave us another service that added this markdown
filter. At the end of the tutorial, we'll even learn how to add our own custom filters.
This filter is immediately useful because we might also want to process the answers through Markdown. We could do that in the controller, but it would be much easier in the template. I'll add some "ticks" around the word "purrrfectly":
// ... lines 1 - 10 | |
class QuestionController extends AbstractController | |
{ | |
// ... lines 13 - 30 | |
public function show($slug, MarkdownParserInterface $markdownParser) | |
{ | |
$answers = [ | |
'Make sure your cat is sitting `purrrfectly` still ?', | |
// ... lines 35 - 36 | |
]; | |
// ... lines 38 - 45 | |
} | |
} |
Then, in show.html.twig
, scroll down to where we loop over the answers. Here, say answer|markdown
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
// ... lines 7 - 36 | |
<ul class="list-unstyled"> | |
{% for answer in answers %} | |
<li class="mb-4"> | |
<div class="d-flex justify-content-center"> | |
// ... lines 41 - 43 | |
<div class="mr-3 pt-2"> | |
{{ answer|markdown }} | |
// ... line 46 | |
</div> | |
// ... lines 48 - 52 | |
</div> | |
</li> | |
{% endfor %} | |
</ul> | |
</div> | |
{% endblock %} |
And because answers will eventually be added by users we don't trust, in a real app, I would use answer|striptags|markdown
. Cool, right? That would remove any tags HTML added by the user and then processes it through Markdown.
Anyways, let's try it! Refresh and... got it! This filter is smart enough to automatically not escape the HTML, so we don't need |raw
.
Next: I'm loving this idea of finding new tools - I mean services - and seeing what we can do with them. Let's find another service that's already in our app: a caching service. Because parsing Markdown on every request can slow things down.
Hello, when you use Knp markdown to HTML, it embeds the text in a <p> tag. So
{{ "text"|markdown }}
gives<p>text</p>.
However, it is impossible to have one <p> tag inside another <p> tag. So in the example of this course, it first generates an empty
<p class="d-inline"></p>
, then the <p> tag with the transformed markdown.If we want to keep previous display, we would need to put the class d-inline into the generated <p> tag.