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

Query Joins & Solving the N+1 Problem

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

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

Login Subscribe

Let's start with a little, annoying problem: when we search, the term does not show up inside the search box. That sucks! Go back to the template. Ok, just add value="" to the search field. Now, hmm, how can we get that q query parameter? Well, of course, we could pass a new q variable into the template and use it. That's totally valid.

But, of course, there's a shortcut! In the template, use {{ app.request.query.get('q') }}:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments</h1>
<form>
<div class="input-group mb-3">
<input type="text"
... lines 15 - 16
value="{{ app.request.query.get('q') }}"
... line 18
>
... lines 20 - 25
</div>
</form>
... lines 28 - 58
</div>
</div>
{% endblock %}

Before we talk about this black magic, try it: refresh. It works! Woo!

Back in Twig, I hope you're now wondering: where the heck did this app variable come from? When you use Twig with Symfony, you get exactly one global variable - completely for free - called app. In fact, find your terminal, and re-run the trusty:

php bin/console debug:twig

Yep! Under "Globals", we have one: app. And it's an object called, um, AppVariable. Ah, clever name Symfony!

Back in your editor, type Shift+Shift and search for this: AppVariable. Cool! Ignore the setter methods on top - these are just for setup. The AppVariable has a couple of handy methods: getToken() and getUser() both relate to security. Then, hey! There's our favorite getRequest() method, then getSession(), getEnvironment(), getDebug() and something called "flashes", which helps render temporary messages, usually for forms.

It's not a huge class, but it's handy! We're calling getRequest(), then .query.get(), which ultimately does the same thing as the code in our controller: go to the query property and call get():

... lines 1 - 9
class CommentAdminController extends Controller
{
... lines 12 - 14
public function index(CommentRepository $repository, Request $request)
{
$q = $request->query->get('q');
... lines 18 - 22
}
}

Cool. So now it's time for a totally new challenge. In addition to searching a comment's content and author name, I also want to search the comment's, article's title. For example, if I search for "Bacon", that should return some results.

The Twig For-Else Feature

Oh, by the way, here's a fun Twig feature. When we get zero results, we should probably print a nice message. On a Twig for loop, you can put an else at the end. Add a <td colspan="4">, a centering class, and: No comments found:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
... lines 10 - 28
<table class="table table-striped">
... lines 30 - 37
<tbody>
{% for comment in comments %}
... lines 40 - 55
{% else %}
<tr>
<td colspan="4" class="text-center">
No comments found
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

Go back and try it! It works! Pff, except for my not-awesome styling skills. Use text-center:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
... lines 10 - 28
<table class="table table-striped">
... lines 30 - 37
<tbody>
{% for comment in comments %}
... lines 40 - 55
{% else %}
<tr>
<td colspan="4" class="text-center">
No comments found
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

That's better.

Adding a Join

Anyways, back to the main event: how can we also search the article's title? In SQL, if we need to reference another table inside the WHERE clause, then we need to join to that table first.

In this case, we want to join from comment to article: an inner join is perfect. How can you do this with the QueryBuilder? Oh, it's awesome: ->innerJoin('c.article', 'a'):

... lines 1 - 15
class CommentRepository extends ServiceEntityRepository
{
... lines 18 - 34
public function findAllWithSearch(?string $term)
{
$qb = $this->createQueryBuilder('c')
->innerJoin('c.article', 'a');
... lines 39 - 50
}
... lines 52 - 80
}

That's it. When we say c.article, we're actually referencing the article property on Comment:

... lines 1 - 10
class Comment
{
... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
... lines 37 - 94
}

Thanks to that, we can be lazy! We don't need to explain to Doctrine how to join - we don't need an ON article.id = comment.article_id. Nah, Doctrine can figure that out on its own. The second argument - a - will be the "alias" for Article for the rest of the query.

Before we do anything else, go refresh the page. Nothing changes yet, but go open the profiler and click to look at the query. Yes, it's perfect! It still only selects from comment, but it does have the INNER JOIN to article!

We can now easily reference the article somewhere else in the query. Inside the andWhere(), add OR a.title LIKE :term:

... lines 1 - 15
class CommentRepository extends ServiceEntityRepository
{
... lines 18 - 34
public function findAllWithSearch(?string $term)
{
$qb = $this->createQueryBuilder('c')
->innerJoin('c.article', 'a');
if ($term) {
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term OR a.title LIKE :term')
... line 42
;
}
... lines 45 - 50
}
... lines 52 - 80
}

That's all you need. Move back and refresh again. It works instantly. Check out the query again: this time we have the INNER JOIN and the extra logic inside the WHERE clause. Building queries with the query builder is not so different than writing them by hand.

Solving the N+1 (Extra Queries) Problem

You'll also notice that we still have a lot of queries: 7 to be exact. And that's because we are still suffering from the N+1 problem: as we loop over each Comment row, when we reference an article's data, a query is made for that article.

But wait... does that make sense anymore? I mean, if we're already making a JOIN to the article table, isn't this extra query unnecessary? Doesn't Doctrine already have all the data it needs from the first query, thanks to the join?

The answer is... no, or, at least not yet. Remember: while the query does join to article, it only selects data from comment. We are not fetching any article data. That's why the extra 6 queries are still needed.

But at this point, the solution to the N+1 problem is dead simple. Go back to CommentRepository and put ->addSelect('a'):

... lines 1 - 15
class CommentRepository extends ServiceEntityRepository
{
... lines 18 - 34
public function findAllWithSearch(?string $term)
{
$qb = $this->createQueryBuilder('c')
->innerJoin('c.article', 'a')
->addSelect('a');
... lines 40 - 51
}
... lines 53 - 81
}

When you create a QueryBuilder from inside a repository, that QueryBuilder automatically knows to select from its own table, so, from c. With this line, we're telling the QueryBuilder to select all of the comment columns and all of the article columns.

Try it: head back and refresh. It still works! But, yes! We're down to just one query. Go check it out: yep! It selects everything from comment and article.

The moral of the story is this: if your page has a lot of queries because Doctrine is making extra queries across a relationship, just join over that relationship and use addSelect() to fetch all the data you need at once.

But... there is one confusing thing about this. We're now selecting all of the comment data and all of the article data. But... you'll notice, the page still works! What I mean is, even though we're suddenly selecting more data, our findAllWithSearch() method still returns exactly what it did before: it returns a array of Comment objects. It does not, for example, now return Comment and Article objects.

Instead, Doctrine takes that extra article data and stores it in the background for later. But, the new addSelect() does not affect the return value. That's way different than using raw SQL.

It's now time to cross off a todo from earlier: let's add pagination!

Leave a comment!

25
Login or Register to join the conversation
Alessandro-D Avatar
Alessandro-D Avatar Alessandro-D | posted 3 years ago | edited

Hi,
I am having an issue using QueryBuilder.
I have a Entity\Social (id, name, icon), Entity\UserSocial (id, user[ManyToOne with User entity], social[ManyToOne with Social Entity]) and then a Entity\UserSocial (id, user, [ManyToOne with User entity], social[ManyToOne with Social Entity], url).
In the SocialRepository I created a new method called findAllWithUserValues, but I cannot seems to be able to get the result I want. If ran a raw query (select social.*, us.url from social LEFT JOIN user_social us on social.id = us.social_id AND us.user_id = 1) I get exactly what I need. When I am editing a user profile, I want all social media to be listed there. Out of all, some are associated with the user, having it filled with the user url.
This is my method:
`public function findAllWithUserValues()
{

return $this->createQueryBuilder('s')
    ->select('s.*', 'us.url')
    ->leftJoin('App:UserSocial', 'us', 'on', 's.id = us.social_id AND us.user_id = 1')
    ->getQuery()
    ->getResult();

}`
If I try to get the DQL, I get the query I want:
"<blockquote>SELECT s., us.url FROM App\Entity\Social s LEFT JOIN App:UserSocial us ON s.id = us.social_id AND us.user_id = 1</blockquote>" but when I try ->getQuery()->getResult(), I get an error which doesn't make any sense: <blockquote>[Syntax Error] line 0, col 9: Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got ''</blockquote>

Any Idea?

Reply

Hey Alessandro D.

I think you only have to remove the .* part. DQL, if you want to select all fields of an entity table you do so by saying ->select('alias');. Please try that and let me know if it worked :)

Cheers!

1 Reply
Alessandro-D Avatar
Alessandro-D Avatar Alessandro-D | MolloKhan | posted 3 years ago

Hey Diego,
I realised that s.* was causing the issue, but when I removed it, Docrine gave me an issue in the “ON” conditionType. If I replaced the ON with “WITH”, the error was gone but I didn’t get the reault I wanted.
Would you mind giving some real working example of DQL leftJoin that works? Perhaps an example that select specific fields from joined table, so that I can study it, test it and compare it with what I had,

Thanks,
Alessandro

2 Reply

Hey Alessandro D.

I think what you need to do is this


public function findAllWithUserValues()
{
    return $this->createQueryBuilder('s')
    ->addSelect('us.url')
    ->leftJoin('s.userSocial', 'us')
    ->andWhere(us.user_id = 1)
    ->getQuery()
    ->getResult();
}

Notice how I change select to addSelect so it will select everything from "s" plus the "us.url" field. Also notice how I added the WHERE clause by using andWhere() method

I hope it helps. Cheers!

Reply
Stephan Avatar
Stephan Avatar Stephan | posted 3 years ago | edited

Hi,
I have a problem to start the project. I update my Linux Ubuntu and after launching
<b>php bin/console server:run</b>
I had
`

			In XmlUtils.php line 50:

Extension DOM is required.

`

So, I preferred to delete the project and downloading and unzipping a new project and launching composer install in the start directory. But, now I have this message:
`
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Your requirements could not be resolved to an installable set of packages.

Problem 1

- Installation request for nexylan/slack v2.0.0 -> satisfiable by nexylan/slack[v2.0.0].
- nexylan/slack v2.0.0 requires ext-mbstring * -> the requested PHP extension mbstring is missing from your system.

Problem 2

- Installation request for symfony/framework-bundle v4.0.14 -> satisfiable by symfony/framework-bundle[v4.0.14].
- symfony/framework-bundle v4.0.14 requires ext-xml * -> the requested PHP extension xml is missing from your system.

Problem 3

- Installation request for symfony/debug-bundle v4.0.4 -> satisfiable by symfony/debug-bundle[v4.0.4].
- symfony/debug-bundle v4.0.4 requires ext-xml * -> the requested PHP extension xml is missing from your system.

Problem 4

- symfony/framework-bundle v4.0.14 requires ext-xml * -> the requested PHP extension xml is missing from your system.
- symfony/maker-bundle v1.4.0 requires symfony/framework-bundle ^3.4|^4.0 -> satisfiable by symfony/framework-bundle[v4.0.14].
- Installation request for symfony/maker-bundle v1.4.0 -> satisfiable by symfony/maker-bundle[v1.4.0].

To enable extensions, verify that they are enabled in your .ini files:

- /etc/php/7.4/cli/php.ini
- /etc/php/7.4/cli/conf.d/10-opcache.ini
- /etc/php/7.4/cli/conf.d/10-pdo.ini
- /etc/php/7.4/cli/conf.d/20-apcu.ini
- /etc/php/7.4/cli/conf.d/20-calendar.ini
- /etc/php/7.4/cli/conf.d/20-ctype.ini
- /etc/php/7.4/cli/conf.d/20-exif.ini
- /etc/php/7.4/cli/conf.d/20-ffi.ini
- /etc/php/7.4/cli/conf.d/20-fileinfo.ini
- /etc/php/7.4/cli/conf.d/20-ftp.ini
- /etc/php/7.4/cli/conf.d/20-gettext.ini
- /etc/php/7.4/cli/conf.d/20-iconv.ini
- /etc/php/7.4/cli/conf.d/20-imagick.ini
- /etc/php/7.4/cli/conf.d/20-json.ini
- /etc/php/7.4/cli/conf.d/20-phar.ini
- /etc/php/7.4/cli/conf.d/20-posix.ini
- /etc/php/7.4/cli/conf.d/20-readline.ini
- /etc/php/7.4/cli/conf.d/20-shmop.ini
- /etc/php/7.4/cli/conf.d/20-sockets.ini
- /etc/php/7.4/cli/conf.d/20-sysvmsg.ini
- /etc/php/7.4/cli/conf.d/20-sysvsem.ini
- /etc/php/7.4/cli/conf.d/20-sysvshm.ini
- /etc/php/7.4/cli/conf.d/20-tokenizer.ini
- /etc/php/7.4/cli/conf.d/20-xdebug.ini

You can also run php --ini inside terminal to see which files are used by PHP in CLI mode.
`

Can you help me please?

Reply

Hey @stephansav

Looks like you should enable required php extensions: php-xml, php-mbstring.

Cheers

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 4 years ago

My boss like 5 years ago said its bad to select all fields from the query (select * from table) and I always had to specify them. And still trying doing so, to not be "bad" programmer. Also back then when I searched in the internet, also found saying this is bad. And instead said to select each field which you need, even if you need most of them. But I now see in doctrine you and also other coders select all columns without specifying one by one. I would think this could improve performance a bit, by selecting only columns which you need. So I guess my boss was wrong? not sure why in all cases can it be bad to select all fields. If its not a performance issue, it should not be bad? And when there is performance issue, then can specify which ones to select.

Reply

Hey Lijana Z.

If you will hydrate your results as objects, then, there is no way to select only a few of the fields but if you are writing a raw SQL or working with an array, then it's totally fine to only select the fields that you are going to use. Let's be pragmatic first than perfect :)

Cheers!

1 Reply
Ozornick Avatar

The boss is a little right. But. Think about how to create an entity if you select only a Name without Ids, Dates ... Objects in Symfony will not be created, but you can work with an array in raw code

Reply

Yep, that's why Doctrine doesn't allow to hydrate objects partially

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

I have the exact same problem, the n+1 problem.

I have a category.php entity, which is self referencing:


    /**
     * One Category has Many Categories.
     * @ORM\OneToMany(targetEntity="Category", mappedBy="parent", cascade={"remove"})
     * @ORM\OrderBy({"name" = "DESC"})
     */
    private $children;

    /**
     * Many Categories have One Category.
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
     */
    private $parent;

...

    public function __construct() {
        $this->children = new ArrayCollection();
    }
...

public function getChildren()
    {
        return $this->children;
    }```


And on my /category/list page, doctrine creates a query for every single category, because I use this inside the twig template:

{% for category in categories %}

    ...
    {% for sub_category in category.children %}```

the getChildren method creates a new query for every category item.
The solution seems to be a join with addSelect, BUT this is not possible because it is self referencing, both the mapped_by and inversed_by side is the same table/entity (categories).

<b>How can I prevent doctrine from creating new querys for every item on a ManyToOne relation for a self-referencing table?</b>

<u><b>UPDATE:
</b></u>The join works now, but the <u>problem still exists</u>, doctrine creates a new query for every getChildren Call.
Current source code:

Controller:


    public function index(CategoryRepository $repository)
    {
        $categories = $repository->findAllWithJoin();

        return $this->render('category/index.html.twig', [
            'categories' => $categories,
        ]);
    }

Category Repository:



    /**
     * @return Category[]
     */
    public function findAllWithJoin()
    {
        return $this->getOrCreateQueryBuilder()
            ->leftJoin('c.parent', 'cp')
            ->addSelect('cp')
            ->getQuery()
            ->getResult()
            ;
    }

    private function getOrCreateQueryBuilder(QueryBuilder $qb = null)
    {
        // if qb is passed, return it, otherwise return new query builder
        return $qb ?: $this->createQueryBuilder('c'); // r = table alias for recipe in table
    }

Category Entity:


    /**
     * One Category has Many Categories.
     * @ORM\OneToMany(targetEntity="Category", mappedBy="parent", cascade={"remove"})
     * @ORM\OrderBy({"name" = "DESC"})
     */
    private $children;

    /**
     * Many Categories have One Category.
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
     */
    private $parent;

    public function __construct() {
        $this->children = new ArrayCollection();
    }
...
public function getChildren()
    {
        return $this->children;
    }

Template:


                                {% for category in categories %}
                                        ...
                                        {% for sub_category in category.children %}
                                        ...
Reply

Hey Mike P.

You made a join to the "parent" field but it should be to the "children" field, isn't it? Give it a try and let me know if it works

Cheers!

Reply
Mike P. Avatar

It worked flawlessly! Thanks!

1 Reply
Default user avatar
Default user avatar Marlies Maalderink | posted 4 years ago | edited

Hello,
I was wondering why I can't perform joins from different functions. Let's say, I want to make a selection based on different search parameters, not all of which are mandatory, but they do all require the same join. I want to build this query using several functions which I puzzle together based on the search parameters.

For example, these two functions:

So I have two functions. I call this one first:


    public function addEquipmentSubType(QueryBuilder $builder): QueryBuilder
    {
        return $builder
            ->andWhere(est.code = :code)
            ->leftJoin('q.equipmentSubType', 'est')
            ->addSelect('est')
            ->setParameter('code', '123');
    }

and then this one:


public function search(QueryBuilder $builder): QueryBuilder
{
    return $builder
        ->andWhere('q.projectNumber LIKE :search OR 
            est.name LIKE :search OR ')
}

This triggers an error, saying 'est' is not defined. It seems like the only way to fix this, is to join equipmentSubtype again, but this causes the eventual query to have 2 joins with the same table.

Is there a way to pass those table aliasses?

Reply

Hey Marlies,

Hm, but it should work! So, if I understand you right, you have those 2 methods, and somewhere in your application you call them both, something like:


$builder = $this->createQueryBuilder();
$builder = $this->addEquipmentSubType($builder)
    ->search($builder);

Am I right? Because it should work well. The only reasons I can see why it may fail is when you call those methods in the incorrect order, i.e. call search() first and only then addEquipmentSubType().

Oh, it probably not important, but just in case it is try to call leftJoin() first and only then that where() clause:


    public function addEquipmentSubType(QueryBuilder $builder): QueryBuilder
    {
        return $builder
            ->leftJoin('q.equipmentSubType', 'est')
            ->addSelect('est')
            ->andWhere(est.code = :code)
            ->setParameter('code', '123');
    }

And please, double check your alias name, because it's easy to misprint it like "ets" instead of "est".

If nothing helps, please, show me the code where you call them exactly.

Cheers!

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

Again nice course, but have small problem. I try to do similar search function but for Article with Comment (you have Comments with Article).
But, query is just try to search in articles, how have any comments. Exp. if I didn't search any string, I must see my all articles, but now I see article just how has any comments. How to fix it?
public function SearchString(?string $term)
{
$qb = $this->createQueryBuilder('b')
->innerJoin('b.comments', 'c')
->addSelect('c')
;
if ($term) {
$qb->andWhere('b.text LIKE :value OR c.text LIKE :value')
->setParameter('value', '%' . $term . '%')
;
}
return $qb
->orderBy('b.id', 'DESC')
->getQuery()
->getResult()
;
}

Reply

Hey @Student

If I got you right, you want to find all comments that may contain a string but filtered by an Article as well. Then you will have to do a query like this:


$this->createQueryBuilder('article')
    ->leftJoin('article.comments', 'c')
    ->addWhere('article = :article')
    ->addWhere('c.text = :term')
    ->setParameter('article', $article)
    ->setParameter('term', $term)
    ->getQuery()->getResult();

Cheers!

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

If I have search input in another "block.html.twig" not in "index.html.twig", how I can load {{ app.request.query.get('q') }} ? "app" is empty.

Reply

Hey Student,

Are you sure the "app" variable is empty? Because this variable is global and available in all templates. Or are you trying to customize your form with a custom form theme? The question is: How are you using that "block.html.twig" in "index.html.twig"? Did you include it somehow?

Cheers!

Reply
Shaun M. Avatar
Shaun M. Avatar Shaun M. | posted 5 years ago

How do you avoid additional queries on a list of entities with their relationships? Say, related to this example, I want to list some of the comments on the index of posts, similar to the Facebook news feed. For each post, it performs a separate query on the comments table for each post to get the comments. So if a show 10 posts, it's doing 11 queries.

Eloquent handles this using the 'with' method, which uses an array of post IDs to get all related comments in one query, then it somehow hydrates the relevant post with it's comments. So only 2 queries are needed. Is there a similar way to do this with Doctrine? I'm not sure how I would optimize this manually, since the foreign key ID fields are hidden.

Reply

Hey Shaun M.

Good question, but first of all remember the rule - Do not optimize prematurely.
Now, if you still need to do it, or you are just curious then what you have to do is to create a custom repository method that will fetch all posts but adding a "JOIN" (with a SELECT) to the desired relationship. In this case you will end up with only one query

Cheers!

Reply
Greg B. Avatar
Greg B. Avatar Greg B. | posted 5 years ago

Is there a reason you're using PHPDoc annotations in PHP7? I see that they're more expressive :array vs @return Comments[], but I'm wondering if there's some benefit, ie. from PHPStorm or static analysis or something.

Reply

Hey Greg,

Yeah, there's definitely a benefit in using more expressive annotations in addition with PHP 7 return types, and as you said PhpStorm add more autocompletion for it, i.e. when you use "@return Comment[]" - PhpStorm will know that it's not just an array with unknown values but an array of Comment objects, and when you will iterate over those comments, PhpStorm will suggest you autocompletion for single comment in that foreach.

Cheers!

1 Reply
Greg B. Avatar

I've been getting that with a /** @var Comment $comment */ right above all of my foreach loops. I guess it's an extra line or two in one place vs. another... if only PHPStorm were even more magical!

Reply

Hey Greg,

Yes, the "/** @var Comment $comment */" line may work too, but if you need to iterate over those comments in a few spots - you'll have a few lines. Or you can do it once above the method, and in all spots you'll have autocompletion which is much better, and easier to refactor in the future, you'll need to tweak only one spot :)

Cheers!

Reply
Cat in space

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

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!

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-paginator-bundle": "^2.7", // v2.7.2
        "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
        "twig/extensions": "^1.5" // v1.5.1
    },
    "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