Type hint a Query Bus in PHP
Tags: php, typehint, psalm, phpstan
Published: 2023-10-25 08:00:00

## Introduction

If we use a Query Bus[^1] in our application, we usually have an interface that defines a Query:

```php
<?php

declare(strict_types=1);

namespace rubenrubiob\Application;

interface Query
{
}
```

And another interface that defines the Query Bus:

```php
<?php

declare(strict_types=1);

namespace rubenrubiob\Infrastructure\QueryBus;

use rubenrubiob\Application\Query;
use Throwable;

interface QueryBus
{
    public function __invoke(Query $query): mixed;
}
```

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

```php
<?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,
    ){
    }
}

```

This Query is called from a controller as follows:

```php
<?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;
    }
}


```

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](https://stitcher.io/blog/generics-in-php-1) started to get traction in PHP. [PHPStorm is constantly adding more support for them](https://blog.jetbrains.com/phpstorm/tag/generics/), 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
<?php

declare(strict_types=1);

namespace rubenrubiob\Application;

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

```

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
<?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;
}

```

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
```

### `GetBookDTOByIdQuery`

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

```php
<?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,
    ){
    }
}

```

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
```

We can see that PHPStorm also gets our type correctly:


![PHPStorm type hinting the return of the QueryBus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0chh7hsykkpc35z469t8.png)

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](https://plugins.jetbrains.com/plugin/7219-symfony-support).

## 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](https://matthiasnoback.nl/2019/06/you-may-not-need-a-query-bus/), especially the comments section.