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
{
}
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;
}
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,
){
}
}
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;
}
}
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
{
}
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;
}
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
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:
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
.
-
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)