Home >Backend Development >PHP Tutorial >Creating focused domain applications. A Symfony approach (Part 1)
This is the first post of a series that i've decided to create in order to explain how I organize my symfony applications and how I try to write code as domain-oriented as possible.
Bellow, you can find the flow diagram that I will use during all the series parts. In each post, I will focus in a concrete part of the diagram and I will try to analyze the processes involved and detect which parts would belong to our domain and how to decouple from the other parts using external layers.
In this first part, we will focus on the data extraction and validation processes. For the data extraction process, we will assume that the request data comes formatted as JSON.
Based on the fact that we know that the request data comes within the JSON request payload, extracting the request payload (in this case extracting means obtaining an array from the JSON payload) would be as easy as using the php json_decode function.
class ApiController extends AbstractController { #[Route('/api/entity', name: 'api_v1_post_entity', methods: ['POST'])] public function saveAction(Request $request,): JsonResponse { $requestData = json_decode($request->getContent(), true); // validate data } }
To validate the data, we need three elements:
For the first one, we are going to create a DTO (Data Transfer Object) which will represent the incoming data and we will use the Symfony validation constraints attributes to specify how this data must be validated.
readonly class UserInputDTO { public function __construct( #[NotBlank(message: 'Email cannot be empty')] #[Email(message: 'Email must be a valid email')] public string $email, #[NotBlank(message: 'First name cannot be empty')] public string $firstname, #[NotBlank(message: 'Last name cannot be empty')] public string $lastname, #[NotBlank(message: 'Date of birth name cannot be empty')] #[Date(message: 'Date of birth must be a valid date')] public string $dob ){} }
As you can see, we have defined our input data validation rules within the recently created DTO. These rules are the following:
For the second one, we will use the Symfony normalizer component by which we will be able to map the request incoming data into our DTO.
class ApiController extends AbstractController { #[Route('/api/entity', name: 'api_v1_post_entity', methods: ['POST'])] public function saveAction(Request $request, SerializerInterface $serializer): JsonResponse { $requestData = json_decode($request->getContent(), true); $userInputDTO = $serializer->denormalize($requestData, UserInputDTO::class); } }
As shown above, the denormalize method do the stuff and with a single line of code, we get our DTO filled with the incoming data.
Finally, to validate the data we will rely on the Symfony validator service which will receive an instance of our recently denormalized DTO (which will carry the incoming data) and will validate the data according to the DTO rules.
class ApiController extends AbstractController { #[Route('/api/entity', name: 'api_v1_post_entity', methods: ['POST'])] public function saveAction(Request $request,): JsonResponse { $requestData = json_decode($request->getContent(), true); // validate data } }
So far, we have broken down the process of Extracting and Validating Data into four parts:
The question now is: Which of these parts do belong to our domain?
To answer the question, Let's analyze the processes involved:
1.- Extracting data: This part only uses the "json_decode" function to transform the incoming data from json to array. It does not add business logic so this would not belong to the domain.
2.- The DTO: The DTO contains the properties associated with the input data and how they will be validated. This means that the DTO contains business rules (the validation rules) so it would belong to the domain.
3.- Denormalize data: This part simply uses an infrastructure service (a framework component) to denormalize the data into an object. This does not contains business rules so this would not belong to our domain.
4.- Validating data: In the same way as the Denormalize data process, the validating data process also uses an infraestructure service (a framework component) to validate the incoming data. This does not contains business rules since they are defined in the DTO so it would not be part of our domain either.
After analyzing the last points, we can conclude that only the DTO will be part of our domain. Then, what do we do with the rest of the code?
Personally, I like including this kind of processes (extracting, denormalizing and validating data) into the application layer or service layer. Why ?, let's introduce the application layer.
In short, the application layer is responsible for orchestration and coordination, leaving the business logic to the domain layer. Moreover, it acts as an intermediary between the domain layer and external layers such as the presentation layer (UI) and the infrastructure layer.
Starting from the above definition, we could include the Extracting, Denormalizing and Validating processes into a service in the application layer since:
Perfect, we are going to create an application service to manage this processes. How are we going to do it ? How are we going to manage the responsibilities?
The Single Responsibility Principle (SRP) states that each class should be responsible for only one part of the application's behavior. If a class has multiple responsibilities, it becomes more difficult to understand, maintain, and modify.
How does this affect to us ? Let's analyze it.
So far, we know that our application service must extract, denormalize and validate the incoming data. Knowing this, it is easy to extract the following responsibilities:
Should we split these responsibilities into 3 different services? I do not think so. Let me explain.
As we have seen, each responsibility is managed by an infrastructure function or component:
As the application service can delegate these responsibilities to the infrastructure services, we can create a more abstract responsibility (Process data) and assign it to the application service.
class ApiController extends AbstractController { #[Route('/api/entity', name: 'api_v1_post_entity', methods: ['POST'])] public function saveAction(Request $request,): JsonResponse { $requestData = json_decode($request->getContent(), true); // validate data } }
As shown above, the DataProcessor application service uses the json_decode function and the Symfony normalizer and validator service to process the input request and return a fresh and validated DTO. So we can say that the DataProcessor service:
As you may have noticed, the DataProcessor service throws a Symfony ValidationException when the validation process finds an error. In the next post of this series, we will learn how to apply our business rules to structure the errors and finally present them to the client.
I know that we could remove the DataProcessor service and use the MapRequestPayload as the service application layer to extract, denormalize and validate the data but, given the context of this article, I thought it more convenient to write it this way.
In this first article, we have focused on the Extracting and Validating data processes from the flow diagram. We have listed the tasks involved within this process and we have learned how to detect which parts belong to the domain.
Knowing which parts belong to the domain, we have written an application layer service that connects the infrastructure services which the domain rules and coordinates the extracting and validating data process.
In the next article, we will explore hot to define our business rules to manage exceptions and we will also create a domain service which will be responsible of transforming the Input DTO into a persistible entity.
The above is the detailed content of Creating focused domain applications. A Symfony approach (Part 1). For more information, please follow other related articles on the PHP Chinese website!