Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Doctrine DQL

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Hey there friends! And thanks for joining me for to a tutorial that's all about the nerdery around running queries in Doctrine. It sounds simple... and it is for a while. But then you start adding joins, grouping, grabbing only specific data instead of full objects, counts... and... well... it gets interesting! This tutorial is about deep-diving into all that good stuff - including running native SQL queries, the Doctrine Query Language, filtering collections, fixing the "N + 1" problem, and a ton more.

Woh, I'm pumped. So let's get rolling!

Project Setup

To INSERT the most query knowledge into your brain I highly recommend coding along with me. You can download the course code from this page. After you unzip it, you'll have a start/ directory with the same code that you see here. There's also a nifty README.md file with all the setup instructions. The final step will be to spin over to your terminal, move into the project, and run:

symfony serve -d

to start a built-in web server at https://127.0.0.1:8000. I'll cheat, click that, and... say "hello" to our latest initiative - Fortune Queries. You see, we have this side business running a multi-national fortune cookie distribution business... and this fancy app helps us track all the fortunes we've bestowed onto our customers.

It's exactly 2 pages: these are the categories, and you can click into one to see its fortunes... including how many have been printed. This is a Symfony 6.2 project, and at this point, it couldn't be simpler. We have a Category entity, a FortuneCookie entity, exactly one controller and no fancy queries.

Side note: this project uses MySQL... but almost everything we're going to talk about will work on Postgres or anything else.

Creating our First Custom Repository Method

Speaking of that one controller, here on the home page, you can see that we're autowiring CategoryRepository and using the easiest way to query for something in Doctrine: findAll().

... lines 1 - 5
use App\Repository\CategoryRepository;
... lines 7 - 10
class FortuneController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function index(CategoryRepository $categoryRepository): Response
{
$categories = $categoryRepository->findAll();
return $this->render('fortune/homepage.html.twig',[
'categories' => $categories
]);
}
... lines 22 - 29
}

Our first trick will be super simple, but interesting. I want to re-order these categories alphabetically by name. One simple way to do this is by changing findAll() to findBy(). This is normally used to find items WHERE they match a criteria - something like ['name' => 'foo'].

But... you can also just leave this empty and take advantage of the second argument: an order by array. So we could say something like ['name' => 'DESC'].

But... when I need a custom query, I like to create custom repository methods to centralize everything. Head over to the src/Repository/ directory and open up CategoryRepository.php. Inside, we can add whatever methods we want. Let's create a new one called public function findAllOrdered(). This will return an array... and I'll even advertise that this is an array of Category objects.

... lines 1 - 4
use App\Entity\Category;
... lines 6 - 16
class CategoryRepository extends ServiceEntityRepository
{
... lines 19 - 23
/**
* @return Category[]
*/
public function findAllOrdered(): array
{
}
... lines 31 - 73
}

Before we fill this in, back here... call it: ->findAllOrdered().

... lines 1 - 10
class FortuneController extends AbstractController
{
... line 13
public function index(CategoryRepository $categoryRepository): Response
{
$categories = $categoryRepository->findAllOrdered();
... lines 17 - 20
}
... lines 22 - 29
}

Delightful!

Hello DQL (Doctrine Query Language)

If you've worked with Doctrine before, you're probably expecting me to use the Query Builder. We will talk about that in a minute. But I want to start even simpler. Doctrine works with a lot of database systems like MySQL, Postgres, MSSQL, and others. Each of these has an SQL language, but they're not all the same. So Doctrine had to invent its own SQL-like language called "DQL", or "Doctrine Query Language". It's fun! It looks a lot like SQL. The biggest difference is probably that we refer to classes and properties instead of tables and columns.

Let's write a DQL query by hand. Say $dql equals SELECT category FROM App\Entity\Category as category. We're aliasing the App\Entity\Category class to the string category in much the same way we might alias a table name to something in SQL. And over here, by just selecting category, we're selecting everything, which means it will return Category objects.

And that's it! To execute this, create a Query object with $query = $this->getEntityManager()->createQuery($dql);. Then run it with return $query->getResult().

... lines 1 - 16
class CategoryRepository extends ServiceEntityRepository
{
... lines 19 - 26
public function findAllOrdered(): array
{
$dql = 'SELECT category FROM App\Entity\Category as category';
$query = $this->getEntityManager()->createQuery($dql);
return $query->getResult();
}
... lines 34 - 76
}

There's also a $query->execute(), and while it doesn't really matter, I prefer getResult().

When we go over and try that... nothing changes! It is working! We just used DQL directly to make that query!

Adding the DQL ORDER BY

So... what does it look like to add the ORDER BY? You can probably guess how it starts ORDER BY!

The interesting thing is, to order by name, we're not going to refer to the name column in the database. Nope, our Category entity has a $name property, and that's what we're going to refer to. The column is probably also called name... but it could be called unnecessarily_long_column_name and we would still order by the name property.

The point is, because we have a $name property, over here, we can say ORDER BY category.name.

Oh, and in SQL, using the alias is optional - you can say ORDER BY name. But in DQL, it's required, so we must say category.name. Finally, add DESC.

... lines 1 - 26
public function findAllOrdered(): array
{
... line 29
$query = $this->getEntityManager()->createQuery($dql);
dd($query->getSQL());
... lines 32 - 33
}
... lines 35 - 79

If we reload the page now... it's alphabetical!

The DQL -> SQL Transformation

When we write DQL, behind the scenes, Doctrine converts that to SQL and then executes it. It looks to see which database system we're using and translates it into the SQL language for that system. We can see the SQL with dd() (for "dump and die") $query->getSQL().

... lines 1 - 26
public function findAllOrdered(): array
{
... line 29
$query = $this->getEntityManager()->createQuery($dql);
dd($query->getSQL());
... lines 32 - 33
}
... lines 35 - 79

And... there it is! That's the actual SQL query being executed! It has this ugly c0_ alias, but it's what we expect: it grabs every column from that table and returns it. Pretty cool!

By the way, you can also see the query inside our profiler. If we remove that debug and refresh... down here, we can see that we're making seven queries. We'll talk about why there's seven in a bit. But if we click that little icon... boom! There's the first query! You can also see a pretty version of it, as well as a version you can run. If you have any variables inside WHERE clauses, the runnable version will fill those in for you.

Next: We normally don't write DQL by hand. Instead, we build it with the Query Builder. Let's see what that looks like.

Leave a comment!

4
Login or Register to join the conversation
Coding010 Avatar
Coding010 Avatar Coding010 | posted 2 months ago

I believe Rufnex is referring to the project's folder and files icons. I was having the same question.
But I found it's the Atom Material Icons plugin, which can be installed via the Marketplace.

1 Reply

yeah, you found a correct package =)

1 Reply
Rufnex Avatar

Hey, side question ;o) What phpstrom UI do you use? Greetings.

Reply

Hey Rufnex,

Are you talking about PhpStorm theme? If so, we use Darcula in our videos :)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "beberlei/doctrineextensions": "^1.3", // v1.3.0
        "doctrine/doctrine-bundle": "^2.7", // 2.9.1
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.15.1
        "symfony/asset": "6.2.*", // v6.2.7
        "symfony/console": "6.2.*", // v6.2.10
        "symfony/dotenv": "6.2.*", // v6.2.8
        "symfony/flex": "^2", // v2.2.5
        "symfony/framework-bundle": "6.2.*", // v6.2.10
        "symfony/proxy-manager-bridge": "6.2.*", // v6.2.7
        "symfony/runtime": "6.2.*", // v6.2.8
        "symfony/twig-bundle": "6.2.*", // v6.2.7
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/maker-bundle": "^1.47", // v1.48.0
        "symfony/stopwatch": "6.2.*", // v6.2.7
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.10
        "zenstruck/foundry": "^1.22" // v1.32.0
    }
}
userVoice