Query Object Pattern in Symfony

The Query Object Pattern is a pattern that can be used together with the Repository pattern. A query object is described as an object that represents a database query but without the actual query mechanics. It should only include the description of the query you are trying to execute. All the mechanics for executing the actual query against the database is abstracted. This means that a query object should not be aware of the actual database supplier it is querying against.

Without separate Query Objects, our repository might become awash of querying methods. Each time another querying situation is needed, a new method is added to the repository. Over time this builds up.

As you know, if you want to have clean code, we should avoid having an object with too many methods. We rather want multiple smaller objects that are easier to read and understand. Thus the Query Object pattern comes to the rescue.

In this article, we will go over a practical example of implementing the Query Object Pattern in Symfony.

The Repository

First, we will have to define our Repository before we can show the Query Object Pattern in action. So let us define a basic Repository Interface.

interface ArticleRepositoryInterface
{
    public function find(ArticleId $id): Article;

    public function add(Article $article): void;

    public function remove(Article $article): void;

    public function size(): int;
}

This interface has the usual repository contract. And this contract will stay the same for each repository. It will not be changing in the future. We will not add any extra ‘find’ methods to it. You could thus create a BaseRepository interface and implementation in bigger projects.

It will only be used to add or remove objects to and from the repository. And off course, we will also have a simple find and size method.

A Query Object is born

Next, let us create our first query object. For each querying use case, we should create an interface first.

interface ArticleQueryInterface
{
    public function execute(ArticleId $id): ?Article;
}

This might look like a useless query. Because the find will do exactly the same. But that’s not the point here. Whenever we want to query something, we create a query object. In most cases, they will be a bit more complex than this.

Not let us create a Doctrine implementation. We do this by injecting the EntityManagerInterface and then create our find query.

final class ArticleQuery implements ArticleQueryInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function execute(ArticleId $id): ?Article
    {
        $queryBuilder = $this->entityManager->createQueryBuilder()
            ->select('Article')
            ->from(Article::class, 'Article')
            ->where('Article.id = :articleId')
            ->setParameter('articleId', $id);

        $article = $queryBuilder->getQuery()->getOneOrNullResult();

        return $article;
    }
}

Note: For better abstraction, we could create our own abstraction around the query builder and use it here. But that is not the point of this article.

So now if we want to use this Query to get an article in an API call. We just have to inject the ArticleQueryInterface and use the execute method.

        $articleId = new ArticleId($id);

        $article = $this->articleQuery->execute($articleId);

        $response = new JsonResponse($this->serializer->serialize($article, 'json'), 200, [], true);

As you can see. Creating a Query Object is easy.

Querying multiple results

Querying multiple results is almost identical. Let’s assume we have an API call to get the latest articles.

We first define our interface.

interface FindLatestArticlesQueryInterface
{
    public const LIMIT = 10;

    public function execute(int $limit = self::LIMIT): array;
}

Now we will be creating our Doctrine implementation of this interface.

final class FindLatestArticlesQuery implements FindLatestArticlesQueryInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function execute(int $limit = self::LIMIT): array
    {
        $queryBuilder = $this->entityManager->createQueryBuilder()
            ->select('Article')
            ->from(Article::class, 'Article')
            ->where('Article.publishedAt <= :now')
            ->orderBy('Article.publishedAt', 'DESC')
            ->setMaxResults($limit)
            ->setParameter('now', new \DateTimeImmutable());

        $articles = $queryBuilder->getQuery()->getResult();

        return $articles;
    }
}

And then in our controller, we can use it like this:

        $latestArticles = $this->findLatestArticlesQuery->execute();

This will now return us the 10 latest articles. We could also allow the API to decide the number of articles to return if we want.

Adding extra filters

When using the find method in your repositories. You sometimes will have a lot of variations on a single findBy. For example, we might have findAllLatest and findAllLatestByAuthor. With the query object pattern, we can combine these small variations in one object.

We could extend our FindLatestArticlesQuery with a new byAuthorId method. This will then return us all the latest articles, but only the ones written by a given user.

interface FindLatestArticlesQueryInterface
{
    public const LIMIT = 10;

    public function byAuthorId(UserId $userId): self;

    public function execute(int $limit = self::LIMIT): array;
}
final class FindLatestArticlesQuery implements FindLatestArticlesQueryInterface
{
    /**
     * @var null|UserId
     */
    private $authorId;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function execute(int $limit = self::LIMIT): array
    {
        $queryBuilder = $this->entityManager->createQueryBuilder()
            ->select('Article')
            ->from(Article::class, 'Article')
            ->where('Article.publishedAt <= :now')
            ->orderBy('Article.publishedAt', 'DESC')
            ->setMaxResults($limit)
            ->setParameter('now', new \DateTimeImmutable());

        if ($this->authorId !== null) {
            $queryBuilder
                ->andWhere('Article.authorId = :authorId')
                ->setParameter('authorId', $this->authorId);
        }

        $articles = $queryBuilder->getQuery()->getResult();

        return $articles;
    }

    public function byAuthorId(UserId $userId): FindLatestArticlesQueryInterface
    {
        $this->authorId = $userId;

        return $this;
    }
}

And in our controller, we can then based on query parameters, filter down our find latest API.

        $findLatestArticlesQuery = $this->findLatestArticlesQuery;
        if (isset($queryParams['user'])) {
            $findLatestArticlesQuery->byAuthorId(new UserId($queryParams['user']));
        }
        $latestArticles = $findLatestArticlesQuery->execute();

So instead of using different query objects for each variation of our API. We can apply ‘filters‘. I prefer this over creating multiple find methods in the repository.

Structuring our codebase

Another question you might ask. Where do we actually place all these objects in our codebase?

Well, you can structure your codebase however you like. But I like to keep it distinct between Application and Domain. In Domain the Article entity and its ArticleId object are contained. While in the Application layer I have defined the Repository interfaces and Queries.

But its good practice is to create a Query directory at the same level as your Repository directory.

But it all depends on your project setup. Use whatever structure you prefer or as defined by your project. You could just create a Query directory with all the query interfaces and implementations in it.

Conclusion

Now you will have a good idea of what Query Objects are, and how to use them. I like to use this because it keeps my codebase cleaner and thus easier to read. Having one big repository class just doesn’t feel right for me. And after a while, it also becomes hard to see with hindsight what all the types of queries you have.

It does not mean that what I showed here is the best solution for your situation. You might change some naming differently. Use it only in some situations or maybe use it a bit differently. You should always be critical and think before you start coding.

In the next article, we will take this one step further. We will take a look at how we can use this to return DTO objects instead of entities. You will see its benefits and can decide for yourself which solution you prefer.

As always. Think before you code!