This tutorial has a new version, check it out!

Custom Queries

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

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

Login Subscribe

How do you write custom queries in Doctrine? Well, you're already familiar with writing SQL, and, yea, it is possible to write raw SQL queries with Doctrine. But, most of the time, you won't do this. Instead, because Doctrine is a library that works with many different database engines, Doctrine has its own SQL-like language called Doctrine query language, or DQL.

Fortunately, DQL looks almost exactly like SQL. Except, instead of table and column names in your query, you'll use class and property names. Again, Doctrine really wants you to pretend like there is no database, tables or columns behind the scenes. It wants you to pretend like you're saving and fetching objects and their properties.

Introducing: The Query Builder

Anyways, to write a custom query, you can either create a DQL string directly, or you can do what I usually do: use the query builder. The query builder is just an object-oriented builder that helps create a DQL string. Nothing fancy.

And there's a pretty good example right here: you can add where statements order by, limits and pretty much anything else:

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->orderBy('a.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
... lines 36 - 47
}

One nice thing is that you can do this all in any order - you could put the order by first, and the where statements after. The query builder doesn't care!

Oh, and see this andWhere()?

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
... lines 29 - 33
;
}
... lines 36 - 47
}

There is a normal where() method, but it's safe to use andWhere() even if this is the first WHERE clause. Again the query builder is smart enough to figure it out. I recommend andWhere(), because where() will remove any previous where clauses you may have added... which... can be a gotcha!

DQL - and so, the query builder - also uses prepared statements. If you're not familiar with them, it's a really simple idea: whenever you want to put a dynamic value into a query, instead of hacking it into the string with concatenation, put : and any placeholder name. Then, later, give that placeholder a value with ->setParameter():

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
... lines 30 - 33
;
}
... lines 36 - 47
}

This prevents SQL injection.

Writing our Custom Query

In our case, we won't need any arguments, and I'm going to simplify a bit. Let's say andWhere('a.publishedAt IS NOT NULL'):

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
... lines 29 - 31
;
}
... lines 34 - 45
}

You can totally see how close this is to normal SQL. You can even put OR statements inside the string, like a.publishedAt IS NULL OR a.publishedAt > NOW().

Oh, and what the heck does the a mean? Think of this as the table alias for Article in the query - just like how you can say SELECT a.* FROM article AS a.

It could be anything: if you used article instead, you'd just need to change all the references from a. to article..

Let's also add our orderBy(), with a.publishedAt, DESC:

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
->orderBy('a.publishedAt', 'DESC')
... lines 30 - 31
;
}
... lines 34 - 45
}

Oh, and this is a good example of how we're referencing the property name on the entity. The column name in the database is actually published_at, but we don't use that here.

Finally, let's remove the max result:

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
->orderBy('a.publishedAt', 'DESC')
... lines 30 - 31
;
}
... lines 34 - 45
}

Once you're done building your query, you always call getQuery() and then, to get the array of Article objects, getResult():

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 24
public function findAllPublishedOrderedByNewest()
{
return $this->createQueryBuilder('a')
->andWhere('a.publishedAt IS NOT NULL')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult()
;
}
... lines 34 - 45
}

Below this method, there's an example of finding just one object:

... lines 1 - 14
class ArticleRepository extends ServiceEntityRepository
{
... lines 17 - 34
/*
public function findOneBySomeField($value): ?Article
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

It's almost the same: build the query, call getQuery(), but then finish with getOneOrNullResult().

So, in all normal situations, you always call getQuery(), then you'll either call getResult() to return many rows of articles, or getOneOrNullResult() to return a single Article object. Got it?

Now that our new findAllPublishedOrderedByNewest() method is done, let's go use it in the controller: $repository->, and there it is!

... lines 1 - 15
class ArticleController extends AbstractController
{
... lines 18 - 30
public function homepage(EntityManagerInterface $em)
{
$repository = $em->getRepository(Article::class);
$articles = $repository->findAllPublishedOrderedByNewest();
... lines 35 - 38
}
... lines 40 - 79
}

Let's give it a try! Move over and, refresh! Perfect! The order is correct and the unpublished articles are gone.

Autowiring ArticleRepository

To make this even cooler, let me show you a trick. Instead of getting the entity manager and then calling getRepository() to get the ArticleRepository, you can take a shortcut: just type ArticleRepository $repository:

... lines 1 - 5
use App\Repository\ArticleRepository;
... lines 7 - 16
class ArticleController extends AbstractController
... lines 18 - 31
public function homepage(ArticleRepository $repository)
{
$articles = $repository->findAllPublishedOrderedByNewest();
... lines 35 - 38
}
... lines 40 - 79
}

This works for a simple reason: all of your repositories are automatically registered as services in the container. So you can autowire them like anything else. This is how I actually code when I need a repository.

And when we refresh, no surprise, it works!

Custom queries are a big topic, and we'll continue writing a few more here and there. But if you have something particularly challenging, check out our Go Pro with Doctrine Queries tutorial. That tutorial uses Symfony 3, but the query logic is exactly the same as in Symfony 4.

Next, I want to show you two more tricks: one for re-using query logic between multiple queries, and another super shortcut to fetch any entity with zero work.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.2.7
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}