This course is still being released! Check back later for more chapters.
Querying Classes
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet's see how we can query specific types of ships. Here, we see a list of all starships. But what if we only want to display scouts, or freighters, or mining freighters? There's some DQL for that!
First though, let's add some visual distinction between the different types of ships. So we know which ones are scouts, freighters, etc.
Remember, we can't directly access the discriminator column to get this value. Right now, our only option is to check the instance of the ship returned from findAll and see if it's a freighter, scout, etc. This is easy to do in PHP, but in Twig, not so much...
I'd like to be able to call $starship->getType() and have it return the discriminator value. Here's a trick! Open our starship entity, and in the DiscriminatorMap attribute, cut the map array. In the class, add private const TYPE_MAP =... paste, don't forget the semicolon. Back up in the attribute, set the argument to self::TYPE_MAP:
| // ... lines 1 - 10 | |
| #[ORM\DiscriminatorMap(self::TYPE_MAP)] | |
| abstract class Starship | |
| { | |
| private const TYPE_MAP = [ | |
| 'scout' => Scout::class, | |
| 'freighter' => Freighter::class, | |
| 'mining_freighter' => MiningFreighter::class, | |
| ]; | |
| // ... lines 19 - 122 | |
| } |
Now, down below, add a new method, final public function getType(), this will return a string. We're using final here to prevent any subclasses from modifying the logic.
Our TYPE_MAP is keyed by the discriminator values we want to return, and the values are the class names. To get the discriminator value for the current starship object, we can flip the TYPE_MAP array, so the class names are the keys. Down in getType, return array_flip(self::class)[static::class]. As a refresher, self::class always returns the class where the code is written, so it would return Starship::class for all ships. static::class returns the class of the actual object, so it would return Scout::class for a scout, Freighter::class for a freighter, etc. So, it's important to use static::class for the key here:
| // ... lines 1 - 11 | |
| abstract class Starship | |
| { | |
| // ... lines 14 - 118 | |
| final public function getType(): string | |
| { | |
| return array_flip(self::TYPE_MAP)[static::class]; | |
| } | |
| } |
Updating the UI
With our getType() method ready, we'll update the Twig template to show it. Open templates/main/homepage.html.twig. Before the ship name, I'll add some space, and add {{ ship.type }}:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <main class="flex flex-col lg:flex-row"> | |
| // ... lines 7 - 8 | |
| <div class="px-12 pt-10 w-full"> | |
| // ... lines 10 - 13 | |
| <div class="space-y-5"> | |
| {% for ship in ships %} | |
| <div class="bg-[#16202A] rounded-2xl pl-5 py-5 pr-11 flex flex-col min-[1174px]:flex-row min-[1174px]:justify-between"> | |
| <div class="flex justify-center min-[1174px]:justify-start"> | |
| // ... line 18 | |
| <div class="ml-5"> | |
| // ... lines 20 - 23 | |
| <h4 class="text-[22px] pt-1 font-semibold"> | |
| <a | |
| class="hover:text-slate-200" | |
| href="#" | |
| > | |
| {{ ship.type }} | |
| {{ ship.name }} | |
| </a> | |
| </h4> | |
| // ... lines 33 - 35 | |
| </div> | |
| </div> | |
| // ... lines 38 - 48 | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </main> | |
| {% endblock %} |
Over in our browser, refresh, and sweet! We can see the ship name prefixed with the type. It could look nicer though...
Here's a neat tip: even if your site isn't multilingual, you can still use the translation component. Use these as type strings as keys for your default language and customize them in your translation files.
To keep things simple, I'll just run these through some string filters to make them prettier. Back in the template, add |replace({'_': ' '})|title:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <main class="flex flex-col lg:flex-row"> | |
| // ... lines 7 - 8 | |
| <div class="px-12 pt-10 w-full"> | |
| // ... lines 10 - 13 | |
| <div class="space-y-5"> | |
| {% for ship in ships %} | |
| <div class="bg-[#16202A] rounded-2xl pl-5 py-5 pr-11 flex flex-col min-[1174px]:flex-row min-[1174px]:justify-between"> | |
| <div class="flex justify-center min-[1174px]:justify-start"> | |
| // ... line 18 | |
| <div class="ml-5"> | |
| // ... lines 20 - 23 | |
| <h4 class="text-[22px] pt-1 font-semibold"> | |
| <a | |
| class="hover:text-slate-200" | |
| href="#" | |
| > | |
| {{ ship.type|replace({'_': ' '})|title }} | |
| {{ ship.name }} | |
| </a> | |
| </h4> | |
| // ... lines 33 - 35 | |
| </div> | |
| </div> | |
| // ... lines 38 - 48 | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </main> | |
| {% endblock %} |
This replaces underscores with spaces and uppercases the first letter of each word.
Refresh the browser and... much better!
Filtering the Ships
Now, let's get into the actual filtering. Open up the StarshipRepository and add a new method public function filterShips(), return type: array. Above, add a docblock to specify that this returns an array of Starship's:
| // ... lines 1 - 12 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 15 - 19 | |
| /** | |
| * @return Starship[] | |
| */ | |
| public function filterShips(): array | |
| // ... lines 24 - 55 | |
| } |
Inside, return $this->createQueryBuilder(), alias s. Add ->where('s INSTANCE OF '.Scout::class). This INSTANCE OF is a special DQL operator that checks if the entity is an instance of the specified class or any subclass. Just like PHP's instanceof operator! So this will return all scouts and any subclasses of scout (if we had any).
Finish with ->getQuery()->execute():
| // ... lines 1 - 12 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 15 - 22 | |
| public function filterShips(): array | |
| { | |
| return $this->createQueryBuilder('s') | |
| ->where('s INSTANCE OF '.Scout::class) | |
| ->getQuery() | |
| ->execute() | |
| ; | |
| } | |
| // ... lines 31 - 55 | |
| } |
In MainController::homepage(), switch from using findAll() to filterShips():
| // ... lines 1 - 10 | |
| class MainController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function homepage( | |
| // ... line 15 | |
| ): Response { | |
| $ships = $starshipRepository->filterShips(); | |
| // ... lines 18 - 23 | |
| } | |
| } |
Refresh the homepage and... Nice, we just see the scouts!
Using Parameters
When building queries, to prevent SQL injection, we use parameters instead of hardcoding values in the where clause. So let's do that.
Change the where clause to s INSTANCE OF :class, and below, add ->setParameter('class', Scout::class):
| // ... lines 1 - 12 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 15 - 22 | |
| public function filterShips(): array | |
| // ... line 24 | |
| return $this->createQueryBuilder('s') | |
| ->where('s INSTANCE OF :class') | |
| ->setParameter('class', Scout::class) | |
| ->getQuery() | |
| ->execute() | |
| ; | |
| } | |
| // ... lines 32 - 56 | |
| } |
Refresh the page and... an error. "array_rand cannot be empty". Hmm, ok this is coming from our MainController. We're using array_rand to get a random ship, and it's failing because the $ships array is empty.
It's kind of a bummer, but we can't use the class name directly as a parameter. There is a workaround though!
Back in the StarshipRepository, in setParameter(), for the second argument, use $this->getEntityManager()->getClassMetadata(Scout::class).
| // ... lines 1 - 12 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 15 - 22 | |
| public function filterShips(): array | |
| // ... line 24 | |
| return $this->createQueryBuilder('s') | |
| // ... line 26 | |
| ->setParameter('class', $this->getEntityManager()->getClassMetadata(Scout::class)) | |
| // ... lines 28 - 29 | |
| ; | |
| } | |
| // ... lines 32 - 56 | |
| } |
This returns a special metadata object for the Scout class and is what Doctrine needs to properly handle the INSTANCE OF operator with parameters.
Refresh the browser... and nice, just the scouts again!
NOT INSTANCE OF
Try switching the class to Freighter::class and refresh the homepage.
| // ... lines 1 - 13 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 16 - 23 | |
| public function filterShips(): array | |
| { | |
| return $this->createQueryBuilder('s') | |
| // ... line 27 | |
| ->setParameter('class', $this->getEntityManager()->getClassMetadata(Freighter::class)) | |
| // ... lines 29 - 30 | |
| ; | |
| } | |
| // ... lines 33 - 57 | |
| } |
We now see freighters, but also the mining freighters. This is because in PHP, instance of returns true for the class you're looking at and any subclasses.
To display only the normal freighters, we need another where clause. In our filterShips() method, add ->andWhere('s NOT INSTANCE OF :notclass'). Below, duplicate the setParameter line and change class to notclass and Freighter::class to MiningFreighter::class:
| // ... lines 1 - 14 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 17 - 24 | |
| public function filterShips(): array | |
| { | |
| return $this->createQueryBuilder('s') | |
| // ... line 28 | |
| ->andWhere('s NOT INSTANCE OF :notclass') | |
| // ... line 30 | |
| ->setParameter('notclass', $this->getEntityManager()->getClassMetadata(MiningFreighter::class)) | |
| // ... lines 32 - 33 | |
| ; | |
| } | |
| // ... lines 36 - 60 | |
| } |
Refresh the homepage and... there we go, just the normal freighters!
Unfortunately, if the freighter had 10 subclasses, we'd have to add 10 NOT INSTANCE OF's - one for each subclass. I don't believe there's a more elegant way to achieve this out of the box, but let me know in the comments if you know a way!
Before we move on, head back to the MainController and switch from filterShips back to findAll and confirm that we get everything again:
| // ... lines 1 - 10 | |
| class MainController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function homepage( | |
| // ... line 15 | |
| ): Response { | |
| $ships = $starshipRepository->findAll(); | |
| // ... lines 18 - 23 | |
| } | |
| } |
Next, we'll look at how we can display the specific properties for each type of ship in the Twig template.
Comments
"Houston: no signs of life"
Start the conversation!