Criteria System: Champion Collection Filtering

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

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

... 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:

... 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:

... 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:

... 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():

... 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:

... 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()):

... 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():

... 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()):

... 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():

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}