Buy

Criteria System: Champion Collection Filtering

Filtering a collection from inside of your entity like this is really convenient... but unless you know that you will always have a small number of total scientists... it's likely to slow down your page big.

Ready for a better way?! Introducing, Doctrine's Criteria system: a part of Doctrine that's so useful... and yet... I don't think anyone knows it exists!

Here's how it looks: create a $criteria variable set to Criteria::create():

224 lines src/AppBundle/Entity/Genus.php
... lines 1 - 5
use Doctrine\Common\Collections\Criteria;
... lines 7 - 15
class Genus
{
... lines 18 - 214
public function getExpertScientists()
{
$criteria = Criteria::create()
... lines 218 - 221
}
}

Next, we'll chain off of this and build something that looks somewhat similar to a Doctrine query builder. Say, andWhere(), then Criteria::expr()->gt() for a greater than comparison. There are a ton of other methods for equals, less than and any other operator you can dream up. Inside gt, pass it 'yearsStudied', 20:

224 lines src/AppBundle/Entity/Genus.php
... lines 1 - 5
use Doctrine\Common\Collections\Criteria;
... lines 7 - 15
class Genus
{
... lines 18 - 214
public function getExpertScientists()
{
$criteria = Criteria::create()
->andWhere(Criteria::expr()->gt('yearsStudied', 20))
... lines 219 - 221
}
}

And hey! Let's show off: add an orderBy() passing it an array with yearsStudied set to DESC:

224 lines src/AppBundle/Entity/Genus.php
... lines 1 - 5
use Doctrine\Common\Collections\Criteria;
... lines 7 - 15
class Genus
{
... lines 18 - 214
public function getExpertScientists()
{
$criteria = Criteria::create()
->andWhere(Criteria::expr()->gt('yearsStudied', 20))
->orderBy(['yearsStudied', 'DESC']);
... lines 220 - 221
}
}

This Criteria describes how we want to filter. To use it, return $this->getGenusScientists()->matching() and pass that $criteria:

224 lines src/AppBundle/Entity/Genus.php
... lines 1 - 5
use Doctrine\Common\Collections\Criteria;
... lines 7 - 15
class Genus
{
... lines 18 - 214
public function getExpertScientists()
{
$criteria = Criteria::create()
->andWhere(Criteria::expr()->gt('yearsStudied', 20))
->orderBy(['yearsStudied', 'DESC']);
return $this->getGenusScientists()->matching($criteria);
}
}

That is it!

Now check this out: when we go back and refresh, we get all the same results. But the queries are totally different. It still counts all the scientists for the first number. But then, instead of querying for all of the genus scientists, it uses a WHERE clause with yearsStudied > 20. It's now doing the filtering in the database instead of in PHP.

As a bonus, because we're simply counting the results, it ultimately makes a COUNT query. But if - in our template, for example - we wanted to loop over the experts, maybe to print their names, Doctrine would be smart enough to make a SELECT statement for that data, instead of a COUNT. But that SELECT would still have the WHERE clause that filters in the database.

In other words guys, the Criteria system kicks serious butt: we can filter a collection from anywhere, but do it efficiently. Congrats to Doctrine on this feature.

Organizing Criteria into your Repository

But, to keep my code organized, I prefer to have all of my query logic inside of repository classes, including Criteria. No worries! Open GenusRepository and create a new static public function createExpertCriteria():

34 lines src/AppBundle/Repository/GenusRepository.php
... lines 1 - 8
class GenusRepository extends EntityRepository
{
... lines 11 - 26
static public function createExpertCriteria()
{
... lines 29 - 31
}
}

Tip

Whoops! It would be better to put this method in GenusScientistRepository, since it operates on that entity.

Copy the criteria line from genus, paste it here and return it. Oh, and be sure you type the "a" on Criteria and hit tab so that PhpStorm autocompletes the use statement:

34 lines src/AppBundle/Repository/GenusRepository.php
... lines 1 - 5
use Doctrine\Common\Collections\Criteria;
... lines 7 - 8
class GenusRepository extends EntityRepository
{
... lines 11 - 26
static public function createExpertCriteria()
{
return Criteria::create()
->andWhere(Criteria::expr()->gt('yearsStudied', 20))
->orderBy(['yearsStudied', 'DESC']);
}
}

But wait, gasp! A static method! Why!? Well, it's because I need to be able to access it from my Genus class... and that's only possible if it's static. And also, I think it's fine: this method doesn't make a query, it simply returns a small, descriptive, static value object: the Criteria.

Back inside Genus, we can simplify things $this->getGenusScientists()->matching(GenusRepository::createExpertCriteria()):

223 lines src/AppBundle/Entity/Genus.php
... lines 1 - 16
class Genus
{
... lines 19 - 215
public function getExpertScientists()
{
return $this->getGenusScientists()->matching(
GenusRepository::createExpertCriteria()
);
}
}

Refresh that! Sweet! It works just like before.

Criteria in Query Builder

Another advantage of building the Criteria inside of your repository is that you can use it in a query builder. Imagine that we needed to query for all of the experts in the entire system. To do that we could create a new public function - findAllExperts():

45 lines src/AppBundle/Repository/GenusRepository.php
... lines 1 - 8
class GenusRepository extends EntityRepository
{
... lines 11 - 29
public function findAllExperts()
{
... lines 32 - 35
}
... lines 37 - 43
}

Tip

Once again, this method should actually live in GenusScientistRepository, but the idea is exactly the same :).

But, I want to avoid duplicating the query logic that we already have in the Criteria!

No worries! Just return $this->createQueryBuilder('genus') then, addCriteria(self::createExpertCriteria()):

45 lines src/AppBundle/Repository/GenusRepository.php
... lines 1 - 8
class GenusRepository extends EntityRepository
{
... lines 11 - 29
public function findAllExperts()
{
return $this->createQueryBuilder('genus')
->addCriteria(self::createExpertCriteria())
... lines 34 - 35
}
... lines 37 - 43
}

Finish with the normal getQuery() and execute():

45 lines src/AppBundle/Repository/GenusRepository.php
... lines 1 - 8
class GenusRepository extends EntityRepository
{
... lines 11 - 26
/**
* @return Genus[]
*/
public function findAllExperts()
{
return $this->createQueryBuilder('genus')
->addCriteria(self::createExpertCriteria())
->getQuery()
->execute();
}
... lines 37 - 43
}

How cool is that!?

Ok guys, that's it - that's everything. We just attacked the stuff that really frustrates people with Doctrine and Forms. Collections are hard, but if you understand the mapping and the inverse side reality, you write your code to update the mapping side from the inverse side, and understand a few things like orphanRemoval and cascade, everything falls into place.

Now that you guys know what to do, go forth, attack collections and create something amazing.

All right guys, see you next time.

Leave a comment!

  • 2017-05-10 Pipo

    Hi Giorgio,
    Try to remove the ->orderBy() section to see the error disappear.

    Regards,

  • 2017-02-28 Victor Bocharsky

    Hey Giorgio,

    Hm, I don't any problems with your code, actually, it's exactly the same code as we have in screencast. I wonder, what if you just return "$this->getGenusScientists()" in this "getExpertScientists()" method. Does it fix the error? Because I suppose the problem in another place and the error should remain.

    Cheers!

  • 2017-02-27 Giorgio Pagnoni

    Here it is! Should have posted it with my comment now that I think about it.

    Twig:

    {{ genus.genusScientists|length }} ({{ genus.expertScientists|length }} experts)

    Php:



    public function getExpertScientists(){
    $criteria = Criteria::create()
    ->andWhere(Criteria::expr()->gt('yearsStudied', 20))
    ->orderBy(['yearsStudied', 'DESC']);
    return $this->getGenusScientists()->matching($criteria);
    }

    Thank you!

  • 2017-02-27 Victor Bocharsky

    Hey Giorgio,

    Could you show us the line 20 in your "genus/list.html.twig" template? And the code where you use those Doctrine Criteria which caused this error?

    Cheers!

  • 2017-02-27 Giorgio Pagnoni

    As soon as I switched to Doctrine's Criteria system I started getting this error:


    An exception has been thrown during the rendering of a template ("Notice: Undefined property: AppBundle\Entity\GenusScientist::$1") in
    genus/list.html.twig at line 20.
  • 2017-01-23 weaverryan

    Awesome! And great question :)

  • 2017-01-22 Luc Hamers

    Hello Ryan,

    Thank you for the tip, its works great with an iterator and uasort in the getExpertScientists (well, in my version returning phone numbers). It is OK in this situation to do it in the getter, since a user only has a hand-full of numbers. But it is good to know that it could also work for larger arrays with the mapped-option.

  • 2017-01-21 weaverryan

    Hey Luc!

    Awesome - very happy to hear things make more sense now!

    Now, about your question :). First, yes, if you're using the CollectionType like we do in this tutorial, then the form system will call getExpertScientists in order to know which embedded forms to list on the page (assuming your form field is called expertScientists). Unfortunately, it looks like the Criteria system does *not* support joining :(. This is definitely a bummer - there's an issue about it here: https://github.com/doctrine.... So, the only way I can think to do this is in the old, inefficient way - e.g. in getExpertScientists, loop over all scientists and manually create an array with only the ones you want. This will work fine, unless you have a HUGE list of scientists. If you DO have a huge list, then you would need to do something a bit more clever/ugly, like setting the mapped option to false on that field, and manually setting the data on it in the controller $form['expertScientists']->setData($filteredExpertScientists).

    Let me know if that helps! Cheers!

  • 2017-01-21 Luc Hamers

    You helped me a lot with this series on many2many relationsships, thanks a lot!

    I have one question about the criteria filtering. Would it be possible to order the genus experts in the getExpertScientists method by a field in the user table? I now have a special query builder in the repository class, but I am not sure, if this works with the form system because it (as far as I understand it) would use the getExpertScientists in the genus class, right?

  • 2016-12-26 weaverryan

    Woohoo! Congrats! I understand what you mean - the app won't ever really be finished - when we write the tutorials, we emphasize teaching the important stuff... not necessarily working on all the small, less-important details to get the app fully ready. And then, we also keep adding onto it as more important topic become obvious :). Right now, we don't have anything immediately planned for this tutorial series. However, in the future, we do want to cover deployment, HTTP caching, unit testing, file upload, and a potentially a bunch of other topics. If there's something you feel is missing after going through everything, I'd be eager to know it!

    Cheers!

  • 2016-12-22 somecallmetim27

    Thrilled I got through the series! It was fantastic and instrumental to my building my own Symfony web app. That being said, I'm surprised we got all the way to the end and the tutorial app isn't really finished. Are there going to be more tutorials based on this project?