This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
10.

The Clever Criteria System

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

We've got this super handy $ship->getParts() method: it returns all the parts for our starship. But the fiscal year is coming to a close, and we need to plan our budget. Boring, but necessary: our Ferengi bosses demand it! Most parts are cheap, like the nuts and bolts and duct tape that hold everything together. We're not really worried about those. Instead, I want to quickly return all of our ship's parts that cost more than 50,000 credits.

Sure, we could do a fresh query in our controller for all the starship parts related to the ship where the price is greater than 50,000. But where's the fun in that? I want to stick with our easy $ship->getParts() shortcut. Is that possible?

Adding getExpensiveParts()

Jump into the Starship class and look for the getParts() method. Copy that, paste it below, and rename it to getExpensiveParts(). For now, return everything:

191 lines | src/Entity/Starship.php
// ... lines 1 - 13
class Starship
{
// ... lines 16 - 160
/**
* @return Collection<int, StarshipPart>
*/
public function getExpensiveParts(): Collection
{
return $this->parts;
}
// ... lines 168 - 189
}

Back in our show template, take this for a spin. Change parts to expensiveParts:

79 lines | templates/starship/show.html.twig
// ... lines 1 - 4
{% block body %}
// ... lines 6 - 19
<div class="md:flex justify-center space-x-3 mt-5 px-4 lg:px-8">
// ... lines 21 - 25
<div class="space-y-5">
<div class="mt-8 max-w-xl mx-auto">
<div class="px-8 pt-8">
// ... lines 29 - 58
<h4 class="text-xs text-slate-300 font-semibold mt-2 uppercase">
Expensive Parts ({{ ship.expensiveParts|length }})
</h4>
<ul class="text-sm font-medium text-slate-400 tracking-wide">
{% for part in ship.expensiveParts %}
// ... lines 64 - 71
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

There's no expensiveParts property, but this will call the getExpensiveParts() method that we just crafted.

Filtering Out the Cheap Stuff:

Time to make our method return only the expensive parts. Remember: $this->parts isn't an array – it's a special Collection object with a few tricks up its sleeve. One of these is the filter() method. This executes a callback for every part. If we return true, it includes that part in the final collection. If we return false, it filters it out. So we can just say return $part->getPrice() > 50000;:

193 lines | src/Entity/Starship.php
// ... lines 1 - 13
class Starship
{
// ... lines 16 - 163
public function getExpensiveParts(): Collection
{
return $this->parts->filter(function (StarshipPart $part) {
return $part->getPrice() > 50000;
});
}
// ... lines 170 - 191
}

Done! Except... this is super inefficient. We're still querying for every part related to our starship, then filtering that in PHP. Imagine if we had 50,000 parts, but only 10 of them cost more than 50,000. What a waste! Could we ask Doctrine to change the query so it only grabs the parts related to the starship where the price is greater than 50,000?

The Power of the Criteria Object

Enter the Criteria object. This thing is mighty. Though, I admit, also a bit cryptic. Clear out our logic and instead use $criteria equals Criteria::create()->andWhere(Criteria::expr()->gt('price', 50000)). To use this, return $this->parts->matching($criteria);:

194 lines | src/Entity/Starship.php
// ... lines 1 - 7
use Doctrine\Common\Collections\Criteria;
// ... lines 9 - 14
class Starship
{
// ... lines 17 - 164
public function getExpensiveParts(): Collection
{
$criteria = Criteria::create()->andWhere(Criteria::expr()->gt('price', 50000));
return $this->parts->matching($criteria);
}
// ... lines 171 - 192
}

Now, if you know me, you know I like to keep my query logic organized in my repository classes. But now we have some query logic inside our entity. Is that bad? Not necessarily, but I like to keep things tidy. So let's move this Criteria logic into our repository.

Moving Criteria to the Repository

Over to StarshipPartRepository we go. Anywhere in here, add a public static function: createExpensiveCriteria():

50 lines | src/Repository/StarshipPartRepository.php
// ... lines 1 - 6
use Doctrine\Common\Collections\Criteria;
// ... lines 8 - 12
class StarshipPartRepository extends ServiceEntityRepository
{
// ... lines 15 - 19
public static function createExpensiveCriteria(): Criteria
{
return Criteria::create()->andWhere(Criteria::expr()->gt('price', 50000));
}
// ... lines 24 - 48
}

Why static? Two reasons: one, because we can (we're not using the this variable anywhere inside), and two, because we're going to use this method from the Starship entity and we can't autowire services into entities, so it must be static.

Back in Starship, use this. Delete the Criteria stuff entirely, and replace it with StarshipPartRepository::createExpensiveCriteria():

193 lines | src/Entity/Starship.php
// ... lines 1 - 4
use App\Repository\StarshipPartRepository;
// ... lines 6 - 15
class Starship
{
// ... lines 18 - 165
public function getExpensiveParts(): Collection
{
return $this->parts->matching(StarshipPartRepository::createExpensiveCriteria());
}
// ... lines 170 - 191
}

Combining Criteria with Query Builders

Everything still works like a charm, so let's go a step further and flex our developer muscles. Let's create a method that combines Criteria with QueryBuilders.

Say we want to get a list of all the expensive parts for any Starship. Start by copying the getExpensiveParts() method from Starship. Paste that in StarshipPartRepository. Then return $this->createQueryBuilder('sp'). Add a $limit argument, defaulting to 10. To combine this with a Criteria, say addCriteria(self::createExpensiveCriteria()). Now that we're in a QueryBuilder, we can do the normal stuff, like setMaxResults($limit). Want to do an orderBy or an andWhere? Go for it. And of course, you can finish this with getQuery()->getResult():

63 lines | src/Repository/StarshipPartRepository.php
// ... lines 1 - 13
class StarshipPartRepository extends ServiceEntityRepository
{
// ... lines 16 - 25
/**
* @return Collection<StarshipPart>
*/
public function getExpensiveParts(int $limit = 10): Collection
{
return $this->createQueryBuilder('sp')
->addCriteria(self::createExpensiveCriteria())
->setMaxResults($limit)
->getQuery()
->getResult();
}
// ... lines 37 - 61
}

Combining Criteria with Query Builders is a power move.

Alright, that's enough about that. Next up, we'll create an entirely new page to list every part. We're on our way to needing some JOINs!