DEV Community

Cover image for Type hint a Query Bus in PHP
Rubén Rubio
Rubén Rubio

Posted on

Type hint a Query Bus in PHP

Introduction

If we use a Query Bus1 in our application, we usually have an interface that defines a Query:

<?php

declare(strict_types=1);

namespace rubenrubiob\Application;

interface Query
{
}
Enter fullscreen mode Exit fullscreen mode

And another interface that defines the Query Bus:

<?php

declare(strict_types=1);

namespace rubenrubiob\Infrastructure\QueryBus;

use rubenrubiob\Application\Query;
use Throwable;

interface QueryBus
{
    public function __invoke(Query $query): mixed;
}
Enter fullscreen mode Exit fullscreen mode

In a concrete example, we could have this query that returns a book:

<?php

declare(strict_types=1);

namespace rubenrubiob\Application\Query\Book;

use rubenrubiob\Application\Query;
use rubenrubiob\Domain\DTO\Book\BookDTO;

final readonly class GetBookDTOByIdQuery implements Query
{
    public function __construct(
        public string $id,
    ){
    }
}

Enter fullscreen mode Exit fullscreen mode

This Query is called from a controller as follows:

<?php

declare(strict_types=1);

namespace rubenrubiob\Infrastructure\Ui\Http\Controller;

use rubenrubiob\Application\Query\Book\GetBookDTOByIdQuery;
use rubenrubiob\Domain\DTO\Book\BookDTO;
use rubenrubiob\Infrastructure\QueryBus\QueryBus;

final readonly class GetBookController
{
    public function __construct(private QueryBus $queryBus)
    {
    }

    public function __invoke(string $bookId): BookDTO
    {
        /** @var BookDTO $book */
        $book = $this->queryBus->__invoke(
            new GetBookDTOByIdQuery(
                $bookId,
            )
        );

        return $book;
    }
}


Enter fullscreen mode Exit fullscreen mode

We have a problem with typing because the return type of the QueryBus is mixed, so it could return anything. Does it return an object? An array? It returns nothing?

In this case, we can force the type with a PHPDoc annotation in the variable within the controller. However, this annotation can become obsolete, besides generating inconsistencies in static code analyzers such as Psalm or PHPStan.

But thanks to these analyzers, generics started to get traction in PHP. PHPStorm is constantly adding more support for them, so we can use them within our IDE.

In this post, we will see how to use generics in our QueryBus to get its return type.

Implementation

Query

We will link any Query with the response value of the correspondent QueryHandler. We will use generics, which in PHP are described as template.

We will mark our Query interface as a template:

<?php

declare(strict_types=1);

namespace rubenrubiob\Application;

/** @template T */
interface Query
{
}

Enter fullscreen mode Exit fullscreen mode

A template or generic indicates that we have a variable type, the same way we have regular variables.

In this case, we have a variable type called T. The name is arbitrary. This way, any class that implements the Query interface will implement the generic type, T.

QueryBus

We can now type our QueryBus the following way:

<?php

declare(strict_types=1);

namespace rubenrubiob\Infrastructure\QueryBus;

use rubenrubiob\Application\Query;
use Throwable;

interface QueryBus
{
    /**
     * @template T
     *
     * @param Query<T> $query
     *
     * @return T
     *
     * @throws Throwable
     */
    public function __invoke(Query $query): mixed;
}

Enter fullscreen mode Exit fullscreen mode

As the argument of the invoke method, we receive a Query that incorporates the type T. As we said, T is a variable type, and its name is arbitrary. The return value of the QueryBus will be this variable type that the Query incorporates, T.

If we look at the types, the description of the QueryBus is:

f(Query<T>) => T
Enter fullscreen mode Exit fullscreen mode

GetBookDTOByIdQuery

We can now update the concrete GetBookDTOByIdQuery to set its type:

<?php

declare(strict_types=1);

namespace rubenrubiob\Application\Query\Book;

use rubenrubiob\Application\Query;
use rubenrubiob\Domain\DTO\Book\BookDTO;

/** @implements Query<BookDTO> */
final readonly class GetBookDTOByIdQuery implements Query
{
    public function __construct(
        public string $id,
    ){
    }
}

Enter fullscreen mode Exit fullscreen mode

We can now replace the type of this query in the description of types of the QueryBus:

f(Query<T>) => T
f(Query<BookDTO>) => BookDTO
Enter fullscreen mode Exit fullscreen mode

We can see that PHPStorm also gets our type correctly:

PHPStorm type hinting the return of the QueryBus

This allows us not only to have our code more typed and pass the checks of static analyzers, but it also allows us to autocomplete code in template rendering libraries, such as Twig when using the Symfony plugin.

Conclusions

Using generics, we get to type our QueryBus. By connecting the response type of any Query implementation to the value the handler returns, we get the necessary link that allows us to type hint the QueryBus.

Summary

  • We saw the problems and advantages of type hinting a QueryBus.
  • We explained in a summarized way what generics are.
  • We implemented type hinting for a QueryBus.

  1. We need to ask ourselves if we really need a Query Bus in our application. Why do we need it? The Command Bus pattern allows encapsulating commands between middlewares, that may execute something, such as opening and closing a transaction in the database, for example. But what middleware do we use in a Query Bus? We could substitute the Query Bus for a direct call to the Query Handler and our application would continue to work exactly the same. Being more radical, we can remove both the Query and Query Handler altogether and call directly to the domain service. For a more extended argument, see the following article, especially the comments section. 

Top comments (0)