If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHey! There are rows in our article table! So let's update the news page to not show this hard-coded article, but instead to query the database and print real, dynamic data.
Open ArticleController
and find the show()
method:
... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
... lines 16 - 33 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack) | |
{ | |
if ($slug === 'khaaaaaan') { | |
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...'); | |
} | |
$comments = [ | |
'I ate a normal rock once. It did NOT taste like bacon!', | |
'Woohoo! I\'m going on an all-asteroid diet!', | |
'I like bacon too! Buy some from my site! bakinsomebacon.com', | |
]; | |
$articleContent = <<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF; | |
$articleContent = $markdownHelper->parse($articleContent); | |
return $this->render('article/show.html.twig', [ | |
'title' => ucwords(str_replace('-', ' ', $slug)), | |
'slug' => $slug, | |
'comments' => $comments, | |
'articleContent' => $articleContent, | |
]); | |
} | |
... lines 77 - 88 | |
} |
This renders that page. As I mentioned earlier, DoctrineBundle gives us one service - the EntityManager - that has the power to save and fetch data. Let's get it here: add another argument: EntityManagerInterface $em
:
... lines 1 - 7 | |
use Doctrine\ORM\EntityManagerInterface; | |
... lines 9 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 35 | |
/** | |
* @Route("/news/{slug}", name="article_show") | |
*/ | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 86 | |
} | |
... lines 88 - 99 | |
} |
When you want to query for data, the first step is always the same: we need to get the repository for the entity: $repository = $em->getRepository()
and then pass the entity class name: Article::class
:
... lines 1 - 4 | |
use App\Entity\Article; | |
... lines 6 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
if ($slug === 'khaaaaaan') { | |
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...'); | |
} | |
$repository = $em->getRepository(Article::class); | |
... lines 46 - 86 | |
} | |
... lines 88 - 99 | |
} |
This repository object knows everything about how to query from the article
table. We can use it to say $article = $repository->
. Oh, nice! It has some built-in methods, like find()
where you can pass the $id
to fetch a single article. Or, findAll()
to fetch all articles. With the findBy()
method, you can fetch all articles where a field matches some value. And findOneBy()
is the same, but only returns one Article. Let's use that: ->findOneBy()
and pass it an array with 'slug' => $slug
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
... line 46 | |
$article = $repository->findOneBy(['slug' => $slug]); | |
... lines 48 - 86 | |
} | |
... lines 88 - 99 | |
} |
This will fetch one row where the slug
field matches this value. These built-in find methods are nice... but they can't do much more than this. But, don't worry! We will of course learn how to write custom queries soon.
Above this line, just to help my editor, I'll tell it that this is an Article
object:
... lines 1 - 4 | |
use App\Entity\Article; | |
... lines 6 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
... lines 48 - 86 | |
} | |
... lines 88 - 99 | |
} |
And... hold on, that's important! When you query for something, Doctrine returns objects, not just an associative arrays with data. That's really the whole point of Doctrine! You need to stop thinking about inserting and selecting rows in a database. Instead, think about saving and fetching objects... almost as if you didn't know that a database was behind-the-scenes.
At this point, it's possible that there is no article in the database with this slug. In that case, $article
will be null
. How should we handle that? Well, in the real world, this should trigger a 404 page. To do that, say if !$article
, then, throw $this->createNotFoundException()
. Pass a descriptive message, like: No article for slug "%s"
and pass $slug
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
... lines 51 - 86 | |
} | |
... lines 88 - 99 | |
} |
I want to dig a little bit deeper to see how this work. Hold Command
on a Mac - or Ctrl
otherwise - and click this method. Ah, it comes from a trait
that's used by the base AbstractController
. Fascinating! It just throws an exception!
In Symfony, to trigger a 404, you just need to throw this very special exception class. That's why, in the controller, we throw $this->createNotFoundException()
. The message can be as descriptive as possible because it will only be shown to you: the developer.
After all of this, let's dump()
the $article
to see what it looks like and die
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
dump($article);die; | |
... lines 53 - 86 | |
} | |
... lines 88 - 99 | |
} |
Head back to your browser and first, refresh. Ok! This is the 404 page: there's nothing in the database that matches this slug: all the real slugs have a random number at the end. We see the helpful error message because this is what the 404 page looks like for developers. But of course, when you switch into the prod
environment, your users will see a different page that you can customize.
We're not going to talk about how to customize error pages... because it's super friendly and easy. Just Google for "Symfony customize error pages" and... have fun! You can create separate pages for 404 errors, 403 errors, 500 errors, or whatever your heart desires.
To find a real slug, go back to /admin/article/new
. Copy that slug, go back, paste it and... it works! There is our full, beautiful, well-written, inspiring, Article object... with fake content about meat. Having an object is awesome! We are now... dangerous.
Back in the controller, remove the dump()
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
... lines 51 - 61 | |
} | |
... lines 63 - 74 | |
} |
Keep the hardcoded comments for now. But, remove the $articleContent
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 44 | |
$repository = $em->getRepository(Article::class); | |
/** @var Article $article */ | |
$article = $repository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug)); | |
} | |
$comments = [ | |
'I ate a normal rock once. It did NOT taste like bacon!', | |
'Woohoo! I\'m going on an all-asteroid diet!', | |
'I like bacon too! Buy some from my site! bakinsomebacon.com', | |
]; | |
return $this->render('article/show.html.twig', [ | |
... lines 59 - 60 | |
]); | |
} | |
... lines 63 - 74 | |
} |
Let's also remove the markdown parsing code and the now-unused argument:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 61 | |
} | |
... lines 63 - 74 | |
} |
We'll process the markdown in the template in a minute: Back down at render()
, instead of passing title
, articleContent
and slug
, just pass article
:
... lines 1 - 15 | |
class ArticleController extends AbstractController | |
{ | |
... lines 18 - 38 | |
public function show($slug, SlackClient $slack, EntityManagerInterface $em) | |
{ | |
... lines 41 - 57 | |
return $this->render('article/show.html.twig', [ | |
'article' => $article, | |
'comments' => $comments, | |
]); | |
} | |
... lines 63 - 74 | |
} |
Now, open that template! With the Symfony plugin, you can cheat and hold Command
or Ctrl
and click to open it. Or, it's just in templates/article
.
Updating the template is a dream. Instead of title
, print article.title
:
{% extends 'base.html.twig' %} | |
{% block title %}Read: {{ article.title }}{% endblock %} | |
... lines 4 - 83 |
Oh, and in many cases... but not always... you'll get auto-completion based on the methods on your entity class!
But look closely: it's auto-completing getTitle()
. But when I hit tab, it just prints article.title
. Behind the scenes, there is some serious Twig magic happening. When you say article.title
, Twig first looks to see if the class has a title
property:
... lines 1 - 9 | |
class Article | |
{ | |
... lines 12 - 21 | |
private $title; | |
... lines 23 - 91 | |
} |
It does! But since that property is private, it can't use it. No worries! It then looks for a getTitle()
method. And because that exists:
... lines 1 - 9 | |
class Article | |
{ | |
... lines 12 - 21 | |
private $title; | |
... lines 23 - 43 | |
public function getTitle(): ?string | |
{ | |
return $this->title; | |
} | |
... lines 48 - 91 | |
} |
It calls it and prints that value.
This is really cool because our template code can be simple: Twig figures out what to do. If you were printing a boolean field, something like article.published
, Twig would also look for isPublished()
a hasPublished()
methods. And, if article
were an array, the dot syntax would just fetch the keys off of that array. Twig: you're the bomb.
Let's update a few more places: article.title
, then, article.slug
, and finally, for the content, article.content
, but then |markdown
:
{% extends 'base.html.twig' %} | |
{% block title %}Read: {{ article.title }}{% endblock %} | |
{% 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"> | |
... line 13 | |
<div class="show-article-title-container d-inline-block pl-3 align-middle"> | |
<span class="show-article-title ">{{ article.title }}</span> | |
... lines 16 - 18 | |
<span class="pl-2 article-details"> | |
... line 20 | |
<a href="{{ path('article_toggle_heart', {slug: article.slug}) }}" class="fa fa-heart-o like-article js-like-article"></a> | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="article-text"> | |
{{ article.content|markdown }} | |
</div> | |
</div> | |
</div> | |
... lines 33 - 71 | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 78 - 83 |
The KnpMarkdownBundle gives us a markdown
filter, so that we can just process it right here in the template.
Ready to try it? Move over, deep breath, refresh. Yes! It works! Hello dynamic title! Hello dynamic bacon content!
Oh, and I have a wonderful surprise! The web debug toolbar now has a database icon that tells us how many database queries this page executed and how long they took. But wait, there's more! Click the icon to go into the profiler. Yes! This actually lists every query. You can run "EXPLAIN" on each one or view a runnable query. I use this to help debug when a particularly complex query isn't returning the results I expect.
So, um, yea. This is awesome. Next, let's take a quick detour and have some fun by creating a custom Twig filter with a Twig extension. We need to do this, because our markdown processing is no longer being cached. Boo.
Hey Richard!
Hmm, that's actually a pretty good question! The simplest answer is this: in an earlier tutorial, we installed this bundle specifically to get markdown functionality including this filter. So if you were actually building this project from the beginning, you would be aware that you installed that bundle. And if you had installed that bundle, its docs would tell you all the features it gives you.
But let me give you one other answer :). At any time, you can see a list of ALL the functions & filters in your application by running:
php bin/console debug:twig
This would tell you that you have a markdown
filter. It wouldn't tell you where it came from, but it is nice to get a list of what's available.
I hope that helps! Cheers!
I have a quick question related to:
$article = $repository->findOneBy(['slug' => $slug]);
'slug', is an hard-coded string now, is there a better way to store such strings?
Should this be a constant stored against repository? Example:
$article = $repository->findOneBy([ArticleRepository::COLUMN_SLUG => $slug]);
or maybe entity?
$article = $repository->findOneBy([Article::FIELD_SLUG => $slug])
Does it make sense? What are the best practise here?
Hey Krzysztof K.!
Hmm. This is an interesting question. First, I'll tell you that the best practice is *not* to set these as constants. And, it's not because it's a bad idea. I think it's more because it's seen as unnecessary. In some ways, the string "slug" *is* already a "constant". Well, it's not a constant - it's a *property* name on your class. So what I mean is, this is already represented in concrete code.
But, the "upside" to using a constant would be that, if you wanted to change the property name, that would be very easy. Without a constant, if you wanted to change the property name, you could use "Refactor" on PhpStorm to change *almost* all of the code... but, you're right, that these "queries" would not change. Even still, I *think* it's overkill.
Here is one tip that I have. In practice, I don't use the findOneBy() functions *too* often. Instead, even for simple queries like this, I tend to always make custom repository methods. For example, in ArticleRepository, I might create a findOneBySlug($slug) that does this query. By doing this, instead of having the string "slug" in many places in my codebase, I will only have it in this one class. *This* is the way that I'd centralize this.
Let me know what you think! Cheers!
Whats the best option for escaping data of a search bar (user input), while querying for data, like in this tutorial?
<b>Situtation:</b>
My Query:
$qb->where('MATCH_AGAINST(a.slug, a.description, a.content) AGAINST (:term boolean)>0')
->setParameter('term', '*'.$searchTerm.'*');
If`
$searchTerm`
is i.ex.`
< test`
I get this error`
SQLSTATE[42000]: Syntax error or access violation: 1064 syntax error, unexpected '<', expecting FTS_TERM or FTS_NUMB or '*'`
<b>Question:</b>
How do I prevent this?
How can I tell doctrine to "escape" the :term properly for match_against?
<b>Solution:</b>
I found out, that doctrine really has nothing to offer for this use case.
I have to filter all "operator" characters on $searchTerm myself.
I do this with:
$searchTerm = '*' . preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $searchTerm) . '*';```
Source:
https://stackoverflow.com/a/26537463/4024751
<b>Question:</b>
Is there a way of implementing this "filter" always to my custom DoctrineExtensions\Query\MySQL class?
Source: https://github.com/beberlei/DoctrineExtensions/blob/master/src/Query/Mysql/MatchAgainst.php
I tried it, but in this class I just get the prepared statement at the end:
This:
public function getSql(\Doctrine\ORM\Query\SqlWalker $walker)
...
return sprintf('MATCH (%s) AGAINST (%s)', implode(', ', $fields), $against);```
Just returns AGAINST (%s) as "AGAINST (?)".
Is there any way to apply the new filter the given parameters (%s // :term) automatically inside the custom class?
Or do I really have to do it manually on every call in my repository when using MATCH_AGAINST, because the class itself doesnt have access to the content of :term from setParameters()?
Hey Mike,
Woh, complex question :) Fairly speaking, I think I've never use that MATCH AGAINST thing before, it's difficult to say if it's possible to do with a custom filter... I'd say it should be possible I think. But Doctrine adds an overhead on plain SQL queries to make it work as DQL, so it might be tricky. But I have a workaround I could suggest you: you can create a new method that will do only those 2 things: escaping and adding MATCH AGAINST to the query builder, something like:
public static function addEscapedMatchAgainst(QueryBuilder $qb, $searchTerm)
{
return $qb->where('MATCH_AGAINST(a.slug, a.description, a.content) AGAINST (:term boolean)>0')
->setParameter('term', '*'.$searchTerm.'*');
}
And you can use it wherever you want in your other methods like this:
public function findAllByTerm($searchTerm)
{
$qb = $this->createQueryBuilder('table');
self::addEscapedMatchAgainst($qb, $searchTerm)
}
And as it's a static method - you can even use it in other entity repositories.
I hope this helps!
Cheers!
Hi!
I get the current user
$user = $this->em->getRepository(User::class)->findOneBy(array('id' => $id));
Next, I add access to it with the function
$this->user_model->addExpire($user);
...
public function addExpire(User $user) {
...
}
but a warning occurs in the PHPStorm for the function addExpire
Expected \App\Entity\User, got Object.
How can I remove this warning?
Hey Dmitriy!
Excellent question! Before I give you a long answer & explanation, let's try something simple first ;). What happens if, instead of injecting the entity manager and saying $this->em->getRepository(User::class)
, you injected the UserRepository
service (I assume you have one?) instead and then said $this->userRepository->findOneBy()
?
The first part of this problem is that PHPStorm doesn't know exactly what object $this->em->getRepository(User::class)
returns. It knows that it returns an "entity repository" - but it's not smart enough to know that it is specifically UserRepository
.
Anyways, let me know if that helps. If it doesn't (and it might not depending on your app), I have more to say ;). Also, as a bonus, if you're ever finding something by id, you can just use ->find($id)
instead of findOneBy()
.
Cheers!
Thank you, Ryan!
Maybe I can tell the PHPStorm that the "entity repository" should return a User object.
Maybe something like this:
/**
* @return User[]
*/
But my UserRepository does not contain functions:
find and findOneBy
Hey Dmitriy,
I suppose you can add @method \App\Entity\User|null findOneBy()" annotation to your UserRepositoy class. This annotation should help PhpStorm to know that findOneBy() returns exactly *User* object. If it does not help, as an alternative option you can override that findOneBy() method in UserRepository, add the proper annotation to it to help PhpStorm. Inside this method you would only need to call the parent method like "return parent::findOneBy()" and pass all the same args to it.
If you need an example - let me know! I just think that it's easy to understand and I hope this is clear for you.
Cheers!
Hi Victor. Thanks a lot.
@method \App\Entity\User|null findOneBy()
This is what I was looking for.
Hello during making project the_spacebar I do havr a error :Impossible to access an attribute ("title") on a null variable. Please help me to solve it{% extends 'base.html.twig' %}{% block title %}Read: {{ article.title }}{% endblock %}{% block body %} <div class="container"> <div class="row"> <div class="col-sm-12"> <div class="show-article-container p-3 mt-4">
Hey Mykyta S.
The error is saying that your "article" variable is null
. So, double check what are you passing to your template by adding a dump($article);
right before rendering such template
Cheers!
Hi! I've been following the course and everything is added correctly but I've this error :
<blockquote>"No article for slug "why-asteroids-taste-like-bacon".</blockquote>
Symfony show me the error is in this line :if (!$article) {<br />throw $this->createNotFoundException(sprintf('No article for slug "%s"', $slug));<br />}
Anyone to help me please ^^ ?
Hey Galnash,
Please, double check that you have an Article in the DB with the slug "why-asteroids-taste-like-bacon". Btw, did you load fixtures? If no, you need to run "bin/console doctrine:fixture:load" command to populate the DB with data.
Cheers!
Awesome! I've check my database and I did not have a simple "why-asteroids-taste-like-bacon", only with a random number like this "why-asteroids-taste-like-bacon896".
Thank you for your answer!
I've been following the course carefully, and making sure everything is added correctly, but during this lesson, when I add the query:
$repository = $em->getRepository(Article::class);
I am receiving an error:
Class 'App\Controller\Article' does not exist
Is there anything that I might have missed? I thought I was careful enough, but sometimes things go sideways.
Hey @RayDube
For some reason the import of your Article entity is pointing to the "controllers" folder (App\ "Controller" \Article)
You have to double check 2 things:
- First: the namespace and location of your Article entity
- Second: The import (use statement at the top of your file) of Article entity in your controller's file
Cheers!
That's it, I hadn't noticed that the use statement didn't populate automatically. Should have seem that, must have been tired.
Thanks a million!
// 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-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
},
"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
}
}
simple q I hope: how did you "know" that the knpmarkdownbundle gave you a twig filter? debug_container doesn't show anything related. I guess I am asking how, when developing, you can find this from the command line (I dont use phpstorm, rather emacs) using console? Or is it a case of googling up the official docs for our markdown and hoping they provide twig facilities?