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

Querying for Data!

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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Hey! 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.

Handling 404's

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.

Rendering the Article Data: Twig Magic

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!

See your Queries in the Profiler

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.

Leave a comment!

22
Login or Register to join the conversation
Richard Avatar
Richard Avatar Richard | posted 3 years ago

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?

1 Reply

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!

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 4 years ago

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?

1 Reply

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!

2 Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 3 years ago | edited

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?

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | Mike P. | posted 3 years ago | edited

<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()?

Reply

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!

Reply
Dmitriy Avatar
Dmitriy Avatar Dmitriy | posted 3 years ago

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?

Reply

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!

Reply
Dmitriy Avatar

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

Reply

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!

Reply
Dmitriy Avatar

Hi Victor. Thanks a lot.

@method \App\Entity\User|null findOneBy()

This is what I was looking for.

Reply

Perfect! :)

Cheers!

Reply
Mykyta S. Avatar
Mykyta S. Avatar Mykyta S. | posted 4 years ago | edited

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">

Reply

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!

Reply
Gary L. Avatar
Gary L. Avatar Gary L. | posted 4 years ago | edited

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 ^^ ?

Reply

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!

Reply
Gary L. Avatar

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!

Reply
Default user avatar
Default user avatar RayDube | posted 5 years ago

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.

Reply

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!

1 Reply
Default user avatar

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!

Reply

NP :)
Let's keep learning!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice