Symfony 4 REST API Part 2 – Data Transfer Object

In Part 1 we learned about REST. What REST is and how to implement a simple Restful API with the FOSRestBundle in Symfony. In this article we are going to learn about the nature of a Data Transfer object and how we can implement it in our Restful API.

What is a Data Transfer Object?

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.

It is similar in concepts as the Value Object. Its immutable and has no identity. Although a DTO can exist as a mutable object if you desire so.

In reality a Data Transfer Object does hold the data of an Entity. But that is not a requirement to be a DTO. We can transform or assemble a DTO from and to another object that exists in our application or layer. This means a Data Transfer Object can transfer parts or whole Entities between layers or applications.

Take for example a Request from an external application. This request can be the creation of a whole new Entity. The request thus is an array of properties of the desired Entity. But they can be different in name or construction. The outside world does not know the exact nature of your Domain, but it wants to mutate your model in some way.

The Request itself is immutable. You got that request from the outside world, you can not change that fact. Thus instead of working with arrays we can create a DTO to work with it as an object. This object can then be passed to your application layer and be assembled into a model of your domain.

Then what is the big advantage between an array and an object? Well you know the structure of your objects. You create a strict interface of what properties are required and allowed. When using an array you need to be careful that your data is not modified between layers. And that some external application sends in some properties which should not be changed by the outside world.

Working with Data Transfer Objects gives you more control and makes it easier for the programmer and its users to make less mistakes.

Creating a Data Transfer Object

Now that we know what a DTO object is. Its meaning and nature. We can now go to the practicality and create one. We choose for the immutable version. It is always a good practice to work with immutable data if possible.

<?php

namespace App\Application\DTO;

final class ArticleDTO
{

    private $title;

    private $content;

    public function __construct(string $title, string $content)
    {
        $this->title = $title;
        $this->content = $content;
    }

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

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

}

Replace Request with DTO

Newt we need to replace the Request object in the controller with our DTO.

Before we can do this we need to add the DTO folder to the services.yaml exclude list. We don’t want Data Transfer Objects to be auto wired to our controller. This will cause our application to err.

In the fos_rest.yaml we need to enable the body converter. This will enable the converter to convert the Request object to our Data Transfer Object.

body_converter:
        enabled: true

Now to actually tell Symfony to convert the Request object to our ArticleDTO we can use the @ParamConverter.

@ParamConverter("articleDTO", converter="fos_rest.request_body")

public function postArticle(ArticleDTO $articleDTO): View

So the injected Request object will now be converted trough the ParamConvertor using the fos_rest body convertor to our ArticleDTO object.

Application Service with DTO

Now we can modify the methods of our application service. Instead of reaching the allowed parameter limits or using an ugly array we can now use our ArticleDTO. So whenever we want to extend the properties we want our client to use. We only need to update the ArticleDTO.

But we still need some way to consolidate the mapping of the DTO to an Entity. For this we can create a DTO Assembler as shown below.

final class ArticleAssembler
{

    public function readDTO(ArticleDTO $articleDTO, ?Article $article = null): Article
    {
        if (!$article) {
            $article = new Article();
        }

        $article->setContent($articleDTO->getContent());
        $article->setTitle($articleDTO->getTitle());

        return $article;
    }

    public function updateArticle(Article $article, ArticleDTO $articleDTO): Article
    {
        return $this->readDTO($articleDTO, $article);
    }

    public function createArticle(ArticleDTO $articleDTO): Article
    {
        return $this->readDTO($articleDTO);
    }

    public function writeDTO(Article $article)
    {
        return new ArticleDTO(
            $article->getTitle(),
            $article->getContent()
        );
    }

}

We can also add 2 methods to make it a bit easier to differentiate between creating a new Article object or updating an existing Article object. The implementation of this assembler is your choice. The only thing that matters is that your assembler can convert from your DTO to your Entity, and from your Entity to your DTO. The assembler is also in charge for filling out the missing fields, if any.

Next we implement the assembler in our application service.

public function postArticle(ArticleDTO $articleDTO): View
{
    $article = $this->articleService->addArticle($articleDTO);

    // In case our POST was a success we need to return a 201 HTTP CREATED response with the created object
    return View::create($article, Response::HTTP_CREATED);
}

So we created a Data Transfer Object. And can also assemble it from and to a Article Entity.

Conclusion

So now you learned about how to use a Data Transfer Object with your Rest API. Its not necessary to do this. And in this situation it might be overkill. But in some situation it might be a good practice to use. You could also directly use your Entity instead of the DTO if you desire so. But in that case you always give clients access to the complete Entity. And in most situations you don’t want that. The most important thing to remember is what the different solutions you have. And then choosing the right one in the right situation.