Introduction
In the last post, we analyzed how the serializer and validator symfony components acted as infrastructure services providing us with tools that help us to perform common tasks in applications. We also learned why the UserInputDTO class was the element that belonged to our domain since it contained business rules and how to create an application layer service to perform the extracting and validating data flow.
In this second part, we are going to see how to manage validating errors and, as we did in the first part, we will identify which parts belong to the domain.
The Validation errors
The validation errors are returned by the Symfony validator component after validating the UserInputDTO following the rules established by using the validation constrains.
public function processData(string $content, string $dtoClass): object
{
$requestData = json_decode($content, true);
$userInputDTO = $serializer->denormalize($requestData, UserInputDTO::class);
$errors = $validator->validate($userInputDTO);
if(count($errors) > 0) {
throw new ValidationFailedException($errors);
}
return $userInputDTO
}
As you can see in the code above, if the validation method finds errors, an exception of the type ValidationException is thrown. From here, we must decide how we want to show the errors to the user (domain / business rules) and what tools we will rely on so that the errors reach the user correctly (infrastructure and application).
Centralizing the catch of validation errors
The first thing we have to take into account is that we want to catch the validation errors whenever they occur. To achieve this, we will rely to the infrastructure layer.
The Symfony Kernel comes with a set of built-in kernel events to listen for special events. One of this event is the kernel exception event which is triggered when an exception is thrown. Let's use it for catching the ValidationException errors.
class KernelSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onException'
];
}
public function onException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if($exception instanceof ValidationFailedException){
// Business rules to build the errors
}
}
}
As we can see in the code above, the KernelSubscriber keeps listening to the KernelException event and it will only execute some logic when the catched exception is an instance of the ValidationFailedException class.
From here, we must define the logic that will be executed when the onException method detects that it is a validation error.
Creating a domain service to construct the errors array
As we are in charge of deciding how we are going to structure the errors (we define those business rules), the service which performs the logic will belong to our domain. Let's code it
class ValidationErrorsBuilder {
public function buildErrors(ValidationFailedException $exception): array
{
$errors = [];
foreach ($exception->getViolations() as $violation) {
$errors[$violation->getPropertyPath()] = $violation->getMessage();
}
return $errors;
}
}
The ValidationErrorsBuilder code is pretty simple: it loops the violation errors and creates an associative array where the keys are the properties which generated an error and the values are the error messages.
Using the ValidationErrorsBuilder
Now it's time for using our ValidationErrorsBuilder domain service. We are using it on the KernelSubscriber onException method.
public function onException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if($exception instanceof ValidationFailedException){
$errors = $this->validationErrorsBuilder->buildErrors($exception);
}
}
As you can see, after knowing that the exception is a ValidationFailedException, we use our domain service to get the validation errors array.
Now, let's see the following code:
public function onException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if($exception instanceof ValidationFailedException){
$errors = $this->validationErrorsBuilder->buildErrors($exception);
}
$event->setResponse(new JsonResponse([
'errors' => $errors
], Response::HTTP_BAD_REQUEST));
}
We have added and new line where we set a Symfony JsonResponse holding the errors array as a new response and we specify that the returned HTTP code will be a 400 Bad Request.
We have been relied on the Symfony Response HTTP_BAD_REQUEST constant to specify the response HTTP code. As we are working in a Domain focused environment, we could have been created our custom domain class (for instance a php enum) but, as we only need to handle standard HTTP codes and there is no specific need for customization, we can use the Symfony HTTP codes although this makes us depend a little more on the framework.
And the application layer ?
We have not talked about the application layer so far. We have said at the beginning of the article that the Symfony framework comes with a useful built-in events such as the one we have used: The kernel exception event. Moreover, the symfony framework also offers us the EventSubscriberInterface by which we can create our custom event subscribers and listen to the events we need.
From this information, we can conclude that symfony offers us the kernel exception event and the EventSubscriberInterface but we have to use the interface to create the subscriber specifying which events we are going to listen to. Let's resume:
- The event subscriber specifies that we listen to the Kernel Exception event.
- The event subscriber checks whether the exception is an instance of the ValidationFailedException.
- The event subscriber uses the domain service to build the errors array.
- The event subscriber creates de JsonResponse holding the errors and sets it as a final response.
Does this sound familiar to you? Yes, the event subscriber is responsible for orchestration and coordination of managing the validation errors after the exception is thrown so we could say that the event subscriber would act as an application service.
If we want to go one step further, we could create an application layer service and use it in the subscriber.
class ValidationErrorsProcessor {
public function __construct(
private readonly ValidationErrorsBuilder $validationErrorsBuilder
) {}
public function processErrors(\Throwable $errors): JsonResponse
{
if($exception instanceof ValidationFailedException){
$errors = $this->validationErrorsBuilder->buildErrors($exception);
}
return new JsonResponse([
'errors' => $errors
], Response::HTTP_BAD_REQUEST);
}
}
public function onException(ExceptionEvent $event): void
{
$jsonResponse = $this->validationErrorsProcessor->processErrors($event->getThrowable());
$event->setResponse($jsonResponse);
}
Now, the ValidationErrorsProcessor would act as an application service coordinating the validation errors response management and using the ValidationErrorsBuilder domain service.
Conclusion
In this second article of this series, we have identified what components of the validation errors management process belong to the domain, which elements of the infrastructure we have used and how the Kernel subscriber can act as the application service.
In the next article, we will persist the entity in the database and we will analyze how to separate the logic of transforming the DTO into a persistible entity.
Top comments (2)
Hello Nacho,
I like your reasoning of which code belongs to the domain. Thanks for your ideas!
One remark:
In two pieces of code you create a new JsonResponse with an $errors variable that is not set if $exception is not an instanceof ValidationFailedException. I guess that will throw another exception.
Greetings!
Hey Hans, thanks for commenting.
Yes, If the exception is not a ValidationFailedException, the event response is not modified and the exception continues its path.