Introduction
This is the last article of this series. In the previous article we created an application service which used the UserEntityBuilder service to create the entity. Then, the doctrine entity manager (which is an infrastructure service) was used to persist and flush the entity.
Now, it's time to return a result to the presentation layer.
I would like to remember that we have considered the doctrine entities as a domain entities throughout all the articles in the series. I understand that this is not entirely correct and that it would be better to separate the domain entities from the doctrine entities but, for simplicity, I will finish this article using the doctrine entities as domain entities
I am preparing a new article where I will show how i have structured a full Symfony application and there you will see that the domain entities are completely decoupled from doctrine.
Creating an output DTO and an output builder
Before returning the result to the presentation layer, we need to create a DTO to represent the data we want to return. Let's imagine that we only want to return the email, firstName, lastName and dob parameters. Our output DTO would look like this:
readonly class UserOutputDto {
public function __construct(
public string $email,
public string $firstName,
public string $lastName,
public string $dob,
){}
}
Now that the output DTO is ready, it's time to create a service to build the output from an entity. This service would be part of our domain since we decide what information will be part of the output DTO.
class UserOutputDTOBuilder {
public function build(User $user): UserOutputDto
{
return new UserOutputDto(
$user->getEmail(),
$user->getFirstName(),
$user->getLastName(),
$user->getDob()
);
}
}
The output builder is pretty simple, it creates a UserOutputDto passing to the constructor the parameters from the entity values.
This output builder could be part of the application or presentation layer since it does not contain any logic but I will keep it in the domain as I did with the UserEntityBuilder.
Remember that the UserEntityBuilder did contain some extra logic:
- Generate the token
- Generate the current timestamp
Using the output builder in the UserCreator
If yut remember from the previous post, we leaved the UserCreator createUser return statement empty. Here is where we are going to use the output builder to return an instance of an UserOutputDto.
class UserCreator {
public function __construct(
private readonly UserEntityBuilder $userEntityBuilder,
private readonly EntityManagerInterface $em,
private readonly UserOutputDTOBuilder $userOutputBuilder
){}
public function createUser(UserInputDTO $userInputDto): UserOutputDto
{
$user = $this->userEntityBuilder->buildEntity($userInputDto);
$this->em->persist($user);
$this->em->flush();
return $this->userOutputBuilder->build($user); // Return a DTO ready to be used by the presentation layer
}
}
As you can see, we also have changed the createUser return typehint from object to UserOutputDto.
Returning the data
Finally, it's time to direct the output result towards the presentation layer. In our case, what elements make up the presentation layer?. Taking into account that we are going to generate a Symfony JsonResponse and return it as an HTTP response, the controller would be the element which would represent our presentation layer. Let's return to it.
class ApiController extends AbstractController
{
#[Route('/api/entity', name: 'api_v1_post_entity', methods: ['POST'])]
public function saveAction(Request $request, DataProcessor $dataProcessor, UserCreator $userCreator): JsonResponse
{
$userInputDto = $dataProcessor->processData($request->getContent(), UserInputDTO::class);
$userOutputDto = $userCreator->createUser($userInputDto);
return $this->json($userOutputDto);
}
}
As part of the presentation layer, the symfony controller uses its infrastructure part (the AbstractController json function) to generate a JsonResponse from the output DTO data ready to be returned within a HTTP response.
As you can see, the symfony controller also uses other application services (DataProcessor and UserCreator) to perform the API call process.
Conclusion
In this final article of the series, we explored the process of returning data to the presentation layer in a Symfony application. We began by creating an output Data Transfer Object (DTO) to encapsulate the user data we wanted to return, specifically the email, first name, last name, and date of birth. We then developed a UserOutputDTOBuilder service to construct this DTO from the user entity, emphasizing the importance of defining what information is included in the output.
Finally, we demonstrated how the Symfony controller acts as the presentation layer, utilizing the JsonResponse functionality to return the DTO data as an HTTP response.
If you like my content and enjoy reading it and you are interested in learning more about PHP, you can read my ebook about how to create an operation-oriented API using PHP and the Symfony Framework. You can find it here: Building an Operation-Oriented Api using PHP and the Symfony Framework: A step-by-step guide
Top comments (7)
I think you meant to use
$userOutputDto
in the return statement, not$userInputDto
Also, you may confuse some newer readers because you show the output being constructed byUserOutputDTOBuilder::build()
but then in the controller you're getting the output from$userCreator->createUser()
which is not in any of the previous code blocks shownHey Mark, I am really sorry for the mistake. My bad :(
I forgot to add the section for using the output builder to return the output DTO in the createUser method and also I returned the inputDTO in the controller instead of the output one. I have fixed both mistakes.
Thank you very much for letting me know :)
No worries, just wanted to let you know! 😃 Great series so far! Keep up the good work !
Why do you have an UserInputDTO and an UserOutputDTO? A DTO doesn't care where the data is coming from or going.
In your example the only difference between the DTO's are the validation attributes. I don't see why the validation for the input doesn't apply for the output.
I'm sorry to say that the more I break down your code. The more I am convinced most of the code in the series has nothing to do with domain driven design.
I think you did yourself a disservice by picking the User object. In Symfony this is loaded because it is linked to authentication and authorisation. If you picked a custom thing would you write the same code?
Hey David, A DTO is a data transfer object. I can create a DTO for the input and another for the output. If i want to change the output in the future, this does not affect to the input DTO.
The validation attributes only make sense within the input since you have to ensure that the payload received within the api call is valid. The output DTO is build by your code and, since their values are filled from the database result, there is no need to re-validate them since they were validated before being persisted.
"In Symfony this is loaded because it is linked to authentication and authorisation. If you picked a custom thing would you write the same code? " -> The User entity i am using have nothing to do with the Symfony Security UserInterface. I have used a user entity in the same way as I would have used a car entity.
"I'm sorry to say that the more I break down your code. The more I am convinced most of the code in the series has nothing to do with domain driven design." -> Maybe I should have chose another name to the series, something like "Organizing your symfony services". I'm sorry you didn't like the articles.
The way I view domain design is that there are two layers, infrastructure and domain. In the infrastructure you have multiple domains that communicate with the infrastructure by events.
in this series I only see the DTOs, that provide the events with data, as a part of the domain design.
The
DataProcessor
,UserOutputDTOBuilder
,UserEntityBuilder
and other classes are all part of the infrastructure layer. I think they are abstractions you don't need for domain design.It is not that you have an
UserOutputDto
that triggered my comment, but that it is bound to the creation of the user. There should be an event to display the user that is controlled by the domain.Are you 100% sure that all the data that is required by your domain is stored in the database?
Maybe the ORM gave a saved flag but the actual saving didn't happen. Maybe the validation of the domain user object is not in line with the validation of ORM user object, and that is why data is missing.
I rather be safe than sorry, and that is why I think validation is required for every DTO.
This is how I see a domain design example of the route.
On line 4 the
AppUser
extends the Domain User DTO, to make it easier in the application code to create the DTO.The DTO should be responsible for the validation. This means the
CreateFromrequest
method can return an exception. For a full example there should be try catch code.On line 5 there is a repository method that saves the user in the database based on the user DTO. For a full example add a try catch block to verify the user is saved.
On line 6 a domain class method is called to turn the user DTO in to a display specific DTO. If there are multiple actions you can create a
UserPreparedForSingleDisplay
event and dispatch that.It is not that I didn't like the articles. I think the content doesn't match the title.
But maybe I don't understand what you want to communicate. That is why I added the comments.
I think I understand you and i should had choosen another title. They way i try to organize the code is:
The application service (UserCreator) uses the "UserEntityBuilder" to build the User entity. Then, it uses the doctrine entity manager (the entity manager is for me a infrastructure service) to persist and flush the entity.
The validation is done by the DataProcessor (which is an application service). The validation rules are in the InputDto (as Symfony attributes) and the DataProcessor uses the symfony validator (an infrastructure service) to validate them.
The "UserOutputBuilder" builds the output (the output dto) so that it can be json serialized.
For me, "UserOutputBuilder" and "UserEntityBuilder" are domain services but I understand this may not be a valid approach. I think just changing the title to something like "How I organize my code with symfony" would have been less confusing. Anyway, thanks for your tips.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.