Hydrating Query Objects with DTO’s

In the previous article, we learned how to use Query Objects. But simply returning entities or arrays of entities might not always be the best solution. Instead, it might be better to be hydrating these entities to Data Transfer Objects.

A Data Transfer Object (DTO) is an object used to pass data between different layers in your application. It holds no business data, but only the minimum required data to transfer between layers or applications.
Its immutable and has no identity.

Hydrating a single result

Now let us assume we are going to use the Result Collection interface for both single and multiple results. Some of our queries will return a single result, other queries might return multiple results. The goal is to have one Result Collection interface that is to be used for all our Query Objects.

interface ResultCollectionInterface
{
    public function getSingleResult();
}

Now let us create our implementation. The result collection will accept an array. But we will only be returning a single result in our getSingleResult method.

final class ResultCollection implements ResultCollectionInterface
{
    /**
     * @var array
     */
    private $items;

    public function __construct(array $items)
    {
        $this->items = $items;
    }

    /**
     * This will return a single element. It can be of any return type
     */
    public function getSingleResult()
    {
        return \reset($this->items);
    }
}

Now if we apply this. We still have the same result as before.

Now the real meat is to hydrate this to a DTO.

Construct DTO from an array

A DTO is immutable, so we need to construct it trough the constructor and only provide getter methods. This because we do not want to have any modifications to this object after it has been constructed.

final class ArticleDto implements ConstructFromArrayInterface
{
    use ConstructFromArrayTrait;

    /**
     * @var ArticleId
     */
    private $articleId;

    /**
     * @var string
     */
    private $title;

    /**
     * @var string
     */
    private $content;

    /**
     * ArticleDto constructor.
     * @param ArticleId $articleId
     * @param string $title
     * @param string $content
     */
    public function __construct(ArticleId $articleId, string $title, string $content)
    {
        $this->articleId = $articleId;
        $this->title = $title;
        $this->content = $content;
    }

    /**
     * @return int
     */
    public function getId(): ArticleId
    {
        return $this->articleId;
    }

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this->title;
    }

    /**
     * @return string
     */
    public function getContent(): string
    {
        return $this->content;
    }
}

You might also have noticed that this DTO implements a ConstructFromArrayInterface. This interface defines a static fromArray method.

interface ConstructFromArrayInterface
{
    public static function fromArray(array $array);
}

Instead of manual setting the constructor, we just pass an array and the fromArray method will be dynamically constructing the DTO trough the reflection methods provided by native PHP.

trait ConstructableFromArrayTrait
{
    public static function fromArray(array $data)
    {
        # Construct a reflection method from the constructor and then get all its parameters
        $reflectionMethod = new \ReflectionMethod(static::class, '__construct');
        $reflectionParameters = $reflectionMethod->getParameters();

        $parameters = [];
        # Iterate all the parameters in the constructor and match them with the array keys
        foreach ($reflectionParameters as $reflectionParameter) {
            $parameterName = $reflectionParameter->getName();
            # In case an array key is not found in the constructor, throw an exception
            if (!\array_key_exists($parameterName, $data) && !$reflectionParameter->isOptional()) {
                # In a real project, create your own custom exception class
                throw new \LogicException(
                    'Unable to instantiate \'' . static::class . '\' from an array, argument ' . $parameterName .' is missing.
                     Only the following arguments are available: ' . implode(', ', \array_keys($data)));
            }

            $parameter = $data[$parameterName] ?? $reflectionParameter->getDefaultValue();
            if (\is_array($parameter) && $reflectionParameter->isVariadic()) {
                $parameters = \array_merge($parameters, $parameter);
                continue;
            }

            $parameters[] = $parameter;
        }
        # Create new class with the parameters from the array
        return new static(...$parameters);
    }
}

Now we can add a new method to our ResultCollection class and interface. The hydrateSingleResultAs method will hydrate the result to the given class name.

    /**
     * Hydrate our result to a DTO object
     */
    public function hydrateSingleResultAs(string $className)
    {
        $item = $this->getSingleResult();

        return $className::fromArray($item);
    }

Adapting the Query Object

Now before we can actually use this. We need to ensure our Query Object returns arrays instead of entities hydrated by Doctrine. It makes no sense to let Doctrine hydrate a query result to an entity, then hydrate it to an array, and then to a DTO. This is a waste of time and resources.

So let’s adapt the ArticleQuery.

Instead of requesting the whole entity, we select the columns we want from the database.

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

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

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

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

        return new ResultCollection([$article]);
    }
}

The result of your query will be encapsulated in a ResultCollection object. This allows us to manipulate the result as we see fit. For example, hydrating it to a DTO.

So if we want to hydrate it to an ArticleDTO:

 $articleId = new ArticleId($id);

 $article = $this->articleQuery->execute($articleId)->hydrateSingleResultAs(ArticleDto::class);

Hydrating multiple Results

Now that we know how to hydrate a single query result to a DTO. Let’s take a look at how we can do it for multiple results.

Now let’s assume we have the following Query Object.

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

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

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

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

        return new ResultCollection($articles);
    }
}

In the ResultCollection we are not adding the array of results we get back from the query. We now need some extra methods in ResultCollection to hydrate an array.

    public function hydrateResultsAs(string $className): ResultCollectionInterface
    {
        $hydratedItems = [];
        foreach ($this->items as $item) {
            $hydratedItems[] = $className::fromArray($item);
        }

        return new self($hydratedItems);
    }

Now we can use this method in our controller.

$latestArticles = $this->findLatestArticlesQuery->execute()
->hydrateResultsAs(LatestArticleDTO::class);

This will then return an array of LatestArticleDTO objects. As you can see. We can hydrate our query results to any DTO we need for a given use case. No need to use the slower doctrine hydration.

Conclusion

By hydrating the query results to data transfer objects we can win in performance and readability. We do not have to deal with hydrating entities containing data we do not need. We could also use arrays, but in object-oriented programming, we should always work with objects if possible. This allows us to know what data we can expect. This makes the code easier to read and reason about.

At the end of the day, it is up to you how to implement this. Are you just going to work with entities, or are you going to be using DTO objects? Remember that these DTO can not be changed! So querying data and then changing them is not possible. You will need to implement other strategies for this.

Thus working with DTO’s is another way of thinking and working.

But as always. Think before you code!