Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

KnpMarkdownBundle & Service

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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.

Leave a comment!

38
Login or Register to join the conversation
Antoine R. Avatar
Antoine R. Avatar Antoine R. | posted 1 year ago

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.

1 Reply

Hey Antoine R.!

Ah, good catch on that! That's my fault - I was sloppy! What we should have done is change the <p class="d-inline"></div> into a div to avoid this problem :).

Cheers!

Reply
Antoine R. Avatar

Hello,

I tried that, and even with a div instead, as the generated <p> tag has not the class inline, it still goes below the quote instead of going on the same line ! I think the best way to have the question on the same level as the quote is to use a englobling div with a class that gives display: inline to its child.

Reply

Hey Antoine R.
What you say makes sense to me. Did it work?

Reply
Antoine R. Avatar

Hi,

I just tried it. It works well. However, the element englobing our generated html must be inline too, so I used a span instead of a div.

1 Reply

Great! Thanks for sharing it!

Reply
Boris R. Avatar
Boris R. Avatar Boris R. | posted 1 year ago

Hey :)

In order to get the MarkdownBundle to work, I had to reboot phpstorm. It was not recognized automatically before that. Any reason why ? I guess I missed something.

1 Reply
Zeljko Avatar

Ty. Same problem with Visual Studio Code. Autowiring MarkdownParserInterface didnt work untill I restarted VS. Before you go crazy try restarting :D

Reply

Hey Zeljko!

> Before you go crazy try restarting :D

Haha, that's my favorite, yeah! Thanks for sharing your solution with others :)

Cheers!

Reply

Hey Boris,

Well, PhpStorm caches a lot of files in your project to work faster, and most of the time it regenerate the cache on file change, but sometimes it might not I suppose, especially if you use some kind of virtualization like Docker, etc. So, rebooting may work, though it's really very rare cases I think. Or, sometimes new versions may bring some minor bugs that may cause this - that just happens, that's why keep it up to date is always a good idea :)

Anyway, thank you for sharing your solution with others!

Cheers!

Reply
Jack K. Avatar
Jack K. Avatar Jack K. | posted 1 year ago

Why isn't KnpMarkdownBundle one of Symfony Flex's contrib bundles? There are other bundles from knplabs on there, so I was just wondering.

1 Reply

Hey Jack!

Not every bundle should have a Symfony Flex recipe. Even without any recipe, the bundle will be anyway configured by Symfony Flex: installed and registered in your bundles.php. So, if the bundle does not need any required config, i.e. has good defaults that are rarely changed like in this KnpMarkdownBundle - the maintainers decide to not add a recipe for it - less files in your system, yay! But of course, those who need to configure something - still can do it easily by adding a config file manually.

I hope it's clear for you know!

Cheers!

Reply
ashatou Avatar

Now, I tried following the instructions in the video, but at the end it's the same old text with no bold words. I tried to add code in this comment box but I couldn't. I wanted to continue writing simple text but couldn't get out of Code Block 🙂

  • I have installed the bundle.
  • I am using it in the controller code (got no error when running it).
  • I am using questionText in the twig.

No changes. What am I missing?

Reply
ashatou Avatar

Never mind. It turned out I was editing looking into the wrong page 🙂

Reply

Hey Ashatou,

Great, I'm glad you figured the problem out :) Sometimes it might be also a cache problem, so it's a good idea to clear the cache first to see if it helps ;)

Cheers!

Reply
Tristan N. Avatar
Tristan N. Avatar Tristan N. | posted 4 months ago

Hello,

- When I launch the server with "symfony serve -d" : I can't find a tutorial to solve this : " A new Symfony CLI version is available (5.4.12, currently running 5.4.8)... and then I have a warning : " [WARNING] The local web server is optimized for local development and MUST never be used in a production setup. "

-I don’t know if it’s related but when I launch the web site, symfony profiler announces me 160 deprecations, 1 error, 25 debug...

In advance, thank you

Reply
gazzatav Avatar
gazzatav Avatar gazzatav | posted 4 months ago | edited

@there , hi, now that the markdown bundle from knp is abandoned, do you have any advice on whether to stick with the abandoned bundle or use the underlying michelf package and how to do that? The recommended twig replacement is not a drop-in replacement as the MarkdownParserInterface is not exposed.

Reply

Hey gazzatav!

There's no real harm in sticking with the abandoned bundle... it's SUPER stable, so I wouldn't sweat it too much. If I were to replace it, I would:

A) Install https://github.com/twigphp/...
B) If you only have Michelf installed, then the new Twig filter should magically start using it.
C) If you're rendering markdown in PHP - and need an autowireable service - try registering one manually. I would add:


services:
# ...

Twig\Extra\Markdown\MarkdownInterface: '@twig.markdown.default'

Let me know if that helps - the markdown-extra works well, but it's also simple - so you may need to do more work if you have things customized a bit :).

Cheers!

Reply
Jan Avatar

ok quick question:
"composer require knplabs/knp-markdown-bundle" installed all this thing, but did not add that line into bundles.php
I did that manually and it's ok but maybe anyone knows why is that? I have PHP 8< and Symfony 6< versions

Reply

Hey Jan,

Hm, it should add that line, probably at some point you hit "No" when Composer asked you if you would like to execute a recipe. It might just remember your answer, depends on what you typed, and then do not ask you anymore. Well, not a big deal, adding it manually should be fine too, good catch on how to do it

Cheers!

Reply

Hello. What is the difference from markdown.parser.max and markdown.parser.light? Thanks in advance

Reply

Hey zahariastefan462

The features which are enabled by default. You can find exact configuration here https://github.com/KnpLabs/...

Cheers!

1 Reply
Janek Avatar

Why do not install Markdown service using Flex system? Why KnpMarkdownBundle isn't present in flex.symfony.com?

Reply
sadikoff Avatar sadikoff | SFCASTS | Janek | posted 1 year ago | edited

Hey Janek

Flex doesn't installs services it helps you to configure bundles, which needs some additional configuration. You can read Victor's answer above as it describes perfectly this situation =)

Cheers!

Reply
Norris M. Avatar
Norris M. Avatar Norris M. | posted 1 year ago

Hello! I'm facing an error while running "composer require knplabs/knp-markdown-bundle" this is what I'm getting:

Your requirements could not be resolved to an installable set of packages.

Problem 1
- knplabs/knp-markdown-bundle[1.7.0, ..., 1.7.1] require symfony/framewor
k-bundle ~2.8|~3.0|^4.0 -> found symfony/framework-bundle[v2.8.0-BETA1, ...,
2.8.x-dev, v3.0.0-BETA1, ..., 3.4.x-dev, v4.0.0-BETA1, ..., 4.4.x-dev] but it
conflicts with your root composer.json require (5.2.*).
- knplabs/knp-markdown-bundle[1.8.0, ..., 1.8.1] require php ^7.1.3 -> yo
ur php version (8.0.0) does not satisfy that requirement.
- Root composer.json requires knplabs/knp-markdown-bundle ^1.7 -> satisfi
able by knplabs/knp-markdown-bundle[1.7.0, 1.7.1, 1.8.0, 1.8.1].

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and
removals for packages currently locked to specific versions.

Installation failed, reverting ./composer.json and ./composer.lock to their o
riginal content.

Please anyone advise on how to fix this?

Reply

Hey Norris,

Did you download the course code and start from the start/ directory? Or do you follow this course on your own project? What version of Symfony do you have? Depends on your Symfony version, probably you can specify a specific version of the bundle and install it, looks like Composer unable to resolve it manually. Or as a possible solution you may do what the error message suggest you, i.e. run:

composer require knplabs/knp-markdown-bundle --with-all-dependencies

But this will upgrade your dependencies that sometimes may also require some extra work, depends on the constraints you use in your composer.json file.

Cheers!

1 Reply
Norris M. Avatar
Norris M. Avatar Norris M. | victor | posted 1 year ago

Hi Victor, I used composer require knplabs/knp-markdown-bundle --ignore-platform-reqs and it worked. Thanks for the assistance.

Reply

Hey Norris,

Glad to hear you were able to resolve this, and thanks for sharing your solution with others! For learning purposes it should be ok, but if you want to deploy this project to production someday - it would be better to resolve this problem in a different way, ignoring platform requirements may lead to unexpected problems.

Cheers!

1 Reply
Norris M. Avatar
Norris M. Avatar Norris M. | victor | posted 1 year ago

Hey Victor,

Thanks for the heads up.
Cheers.

Reply
Matthew B. Avatar
Matthew B. Avatar Matthew B. | posted 1 year ago

Great tutorial, but when using the following it rips away the markdown entered. Suggestions?

questionText|striptags|markdown

Reply

Hey Matthew,

Don't use "striptags" filter? :) You literally strip tags first, and then the text without tags you're passing though markdown filter. If it works weird for you - try to debug it e.g. like {{ dump(questionText|striptags) }} - you will see the exact string that will be processed through the markdown filter. Does it look good to you? If no, please share the string and explain what do you mean about "away the markdown entered", I probably don't understand it completely.

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

git is not installed. Are you sure we are not missing any steps? On the previous video I also got an error that it couldnt vind '.' path in ./bin/console

Reply

Hey Farry

Does my other comment helped you out?

Reply
Samuel Avatar

Hey, if I want to use my code on a production server, do I then have to install all the composer packages etc. completly new on this server?

Reply

Hey Samuel

Deploying on a production server is a big topic. But in 2 words you should deploy your code with lock files on server, and run install commands. With this lock files you will be sure that you get same package versions as you have on dev. SO it will work.

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}