Collection Filtering: The Easy Way

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

I have one last cool trick to show you. Go back to /genus.

Oh, but real quick, I need to fix two little things that I messed up before we finish.

Oh my, a Missing inversedBy()

First, see that red label on the web debug toolbar? Click it, and scroll down. It's a mapping warning:

The field User#studiedGenuses property is on the inverse side of a bidirectional relationship, but the association on blah-blah-blah does not contain the required inversedBy().

In human-speak, this says that my User correctly has a studiedGenuses property with a mappedBy option...

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 77
/**
* @ORM\OneToMany(targetEntity="GenusScientist", mappedBy="user")
*/
private $studiedGenuses;
... lines 82 - 221
}

But on GenusScientist, I forgot to add the inversedBy() that points back to this:

... lines 1 - 17
class GenusScientist
{
... lines 20 - 32
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="studiedGenuses")
... line 35
*/
private $user;
... lines 38 - 78
}

I don't really know why Doctrine requires this... since it didn't seem to break anything, but hey! This fixes the warning.

Bad Field Type Mapping!

The second thing I need to fix is this yearsStudied field. When PhpStorm generated the annotation for us, it used type="string"... and I forgot to fix it! Change it to type="integer":

... lines 1 - 17
class GenusScientist
{
... lines 20 - 38
/**
* @ORM\Column(type="integer")
... line 41
*/
private $yearsStudied;
... lines 44 - 78
}

It hasn't caused a problem yet... but it would if we tried to do some number operations on it inside the database.

Of course, we need a migration!

./bin/console doctrine:migrations:diff

Just trust that it's correct - live dangerously:

./bin/console doctrine:migrations:migrate

Sweet! Now go back to /genus.

Fetching a Subset of GenusScientist Results

We're already printing the number of scientists that each Genus has. And thanks to a fancy query we made inside GenusRepository, that joins over and fetches the related User data all at once... this entire page is built with one query:

... lines 1 - 7
class GenusRepository extends EntityRepository
{
/**
* @return Genus[]
*/
public function findAllPublishedOrderedByRecentlyActive()
{
return $this->createQueryBuilder('genus')
->andWhere('genus.isPublished = :isPublished')
->setParameter('isPublished', true)
->leftJoin('genus.notes', 'genus_note')
->orderBy('genus_note.createdAt', 'DESC')
->leftJoin('genus.genusScientists', 'genusScientist')
->addSelect('genusScientist')
->getQuery()
->execute();
}
}

Well, except for the query that loads my security user from the database.

So this is cool! Well, its maybe cool - as we talked about earlier, this is fetching a lot of extra data. And more importantly, this page may not be a performance problem in the first place. Anyways, I want to show you something cool, so comment out those joins:

... lines 1 - 7
class GenusRepository extends EntityRepository
{
... lines 10 - 12
public function findAllPublishedOrderedByRecentlyActive()
{
return $this->createQueryBuilder('genus')
->andWhere('genus.isPublished = :isPublished')
->setParameter('isPublished', true)
->leftJoin('genus.notes', 'genus_note')
->orderBy('genus_note.createdAt', 'DESC')
// ->leftJoin('genus.genusScientists', 'genusScientist')
// ->addSelect('genusScientist')
->getQuery()
->execute();
}
}

Refresh again! Our one query became a bunch! Every row now has a query, but it's a really efficient COUNT query thanks to our fetch EXTRA_LAZY option:

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\OneToMany(
... lines 74 - 75
* fetch="EXTRA_LAZY",
... lines 77 - 78
* )
... line 80
*/
private $genusScientists;
... lines 83 - 209
}

Here's my new wild idea: any scientist that has studied a genus for longer than 20 years should be considered an expert. So, in addition to the number of scientists I also want to print the number of expert scientists next to it.

Look inside the list template: we're printing this number by saying genus.genusScientists|length:

... lines 1 - 2
{% block body %}
<table class="table table-striped">
... lines 5 - 12
<tbody>
{% for genus in genuses %}
<tr>
... lines 16 - 21
<td>{{ genus.genusScientists|length }}</td>
... line 23
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

In other words, call getGenusScientists():

... lines 1 - 14
class Genus
{
... lines 17 - 202
/**
* @return ArrayCollection|GenusScientist[]
*/
public function getGenusScientists()
{
return $this->genusScientists;
}
}

Fetch the results, and then count them:

But how could we filter this to only return GenusScientist results that have studied the Genus for longer than 20 years?

It's easy! In Genus, create a new public function called getExpertScientists():

... lines 1 - 14
class Genus
{
... lines 17 - 213
public function getExpertScientists()
{
... lines 216 - 218
}
}

Then, we'll loop over all of the scientists to find the experts. And actually, we can do that very easily by saying $this->getGenusScientists()->filter(), which is a method on the ArrayCollection object. Pass that an anonymous function with a GenusScientist argument. Inside, return $genusScientist->getYearsStudied() > 20:

... lines 1 - 14
class Genus
{
... lines 17 - 213
public function getExpertScientists()
{
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) {
return $genusScientist->getYearsStudied() > 20;
});
}
}

This will loop over all of the genus scientists and return a new ArrayCollection that only contains the ones that have studied for more than 20 years. It's perfect!

To print this in the template, let's add a new line, then {{ genus.expertScientists|length }} and then "experts":

... lines 1 - 2
{% block body %}
<table class="table table-striped">
... lines 5 - 12
<tbody>
{% for genus in genuses %}
<tr>
... lines 16 - 21
<td>
{{ genus.genusScientists|length }}
({{ genus.expertScientists|length }} experts)
</td>
... line 26
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

Try it! Refresh! Zero! What!? Oh... I forgot my return statement from inside the filter function. Lame!

... lines 1 - 14
class Genus
{
... lines 17 - 213
public function getExpertScientists()
{
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) {
return $genusScientist->getYearsStudied() > 20;
});
}
}

Try it now. Yes!

Click to check out the queries. It still makes a COUNT query for each row... but wait: it also queries for all of the genus_scientist results for each genus. That sucks! Even if a Genus only has two experts... we're fetching all of the data for all of its genus scientists.

Why? Well, as soon as we loop over genusScientists:

... lines 1 - 14
class Genus
{
... lines 17 - 213
public function getExpertScientists()
{
return $this->getGenusScientists()->filter(function(GenusScientist $genusScientist) {
return $genusScientist->getYearsStudied() > 20;
});
}
}

Doctrine realizes that it needs to go and query for all of the genus scientists for this Genus. Then, we happily loop over them to see which ones have more than 20 yearsStudied.

This may or may not be a huge performance problem. If every Genus always has just a few scientists, no big deal! But if a Genus has hundreds of scientists, this page will grind to a halt while it queries for and hydrates all of those extra GenusScientist objects.

There's a better way: and it uses a feature in Doctrine that - until recently - even I didn't know existed. And I'm super happy it does.

Leave a comment!

  • 2019-05-10 Vladimir Sadicov

    Awesome! This is one of real super powers of Symfony! You choose the way you want to code =)

    Cheers!

  • 2019-05-10 Peter Kosak

    I decided to go the unmapped field way

  • 2019-05-10 Vladimir Sadicov

    Hey Peter Kosak

    Yeah you have a tricky situation. As I know this cannot be done automatically because this fields are not synced, to sync them you can create Doctrine entity listener, and listen to PreUpdate and probable PrePersist and check your list changes and sync lists.

    Hope this will help. Cheers!

  • 2019-05-08 Peter Kosak

    I am struggling with my scenario:

    Imagine you have a Teacher, Subject, Room entities.

    Teacher can teach many Subjects
    Teacher can teach in many Rooms

    Subject can be tough in many Rooms so rooms and subjects are linked but 1 Room can have 1 subject. OneToMany

    Now imagine I want to create a Teacher so I will select Subjects(teacherSubjects = arrayCollection)
    After subjects have been added I can pre-filter Rooms available and store them (teacherRooms = arrayCollection)

    Now I will remove subject from the removeSubject and click save.

    I want automatically remove all associated Rooms from teacherRooms.

    I tried to do it inside removeSubject but when form is submitted it is not even triggering this method same is happening when I am adding new subject method addSubject is not hit.

    I know I could probably solve this if I decide to set this field as not mapped but I would assume this can be done automatically.

  • 2019-03-14 Victor Bocharsky

    Hey Michael,

    Yeah, that's a little-known fact. You may even notice a pencil icon on course chapter pages in the right top corner of scripts. If you press it - it will move you to the corresponding page on GitHub.

    About that [[[ code('..') ]]] syntax - it's our own, we implemented it internally. It is dynamically replaced with actual code blocks, but we use an internal tool that helps to display specific code on different steps. It helps us to show the *actual* code on chapter pages - no more outdated static code blocks. And that's the one of our coolest features on SymfonyCasts. :)

    Cheers!

  • 2019-03-14 Tac Tacelosky

    Great, I didn't know the course content was available on gitub. I'm not familiar with the github [[[ code('..') ]]], how is that set up? (Searching for 'github code' gets everything!). Is it a gist? Or something you tag in the code somehow?

  • 2019-03-14 Victor Bocharsky

    Hey Michael,

    Ah, great catch! Thank you for pointing it out! It was fixed in https://github.com/knpunive...

    Cheers!

  • 2019-03-13 Tac Tacelosky

    Typo: "

    Oh my, a Missing inversedBt" should be inverseBy