Data validation is one of the most important parts of all Software Applications. If you are using PHP Symfony Framework, you have available a very powerful yet easy to use validator* component that validates objects against constraints. There is a large set of Constraints ready to use and even you can define your own custom constraints when using this component.
If you are implementing an Application as a Rest Api I recommend you first take a look at Using DTOs in PHP Symfony
Using DTOs and the Param Converter in Symfony allows you to do Data validation in a very easy way, and very fast to implement, even if you have a large set of Api Endpoints consuming many different payloads, because you are validating all payloads in the same function.
Make a DTO to represent the structure of the payload. This is the first thing to validate, that the payload in the Api Request, most likely a json string, has exactly the same structure as defined in the DTO. I do this using JMS Serializer
...
$json = $request->getContent();
$class = $configuration->getClass();
//Try to deserialize the request dto
try {
$dto = $this->serializer->deserialize($json, $class, AppConstants::API_SERIALIZE_FORMAT);
} catch (\Throwable $e) {
$jsonMessage = $this->customValidationServiceProvider->getJsonErrorMessage(json_last_error());
throw new JsonException($e->getMessage() . ' more info:' . $jsonMessage);
}
...
In here the JMS Serializer will try to map the json string in the body of the request (payload) into its corresponding DTO class. In this case
AppConstants::API_SERIALIZE_FORMAT
is 'json' format.
If the de-serialization is successful, then we make the validator component to validate the DTO:
...
//If there are validation errors, throw validation exception
$errors = $this->validator->validate($dto);
if (!empty($errors)) {
foreach ($errors as $error) {
$this->validationErrors[] = array('field' => $error->getPropertyPath(), ' message' => $error->getMessage());
}
if (!empty($this->validationErrors)) {
throw new PreconditionFailedException($this->serializer->serialize($this->validationErrors, AppConstants::API_SERIALIZE_FORMAT));
}
}
...
The message set for each error thrown is the one you declared in each constraint in the DTO:
...
/**
* @var string valid Email
* @Assert\NotNull
* @Assert\NotBlank(message="Email Address required")
* @Assert\Email(message="Invalid Email Address")
* @JMS\Type("string")
* @JMS\SerializedName("email")
*/
public $email;
...
Most types of validation constraints* are already available including one that allows you to define a regular expression with your own rules:
...
/**
* @var string Role Name
* @Assert\NotNull
* @Assert\NotBlank(message="Role name cannot be blank")
* @Assert\Regex(pattern="/^[\w]+$/", message="Role names can only contain letters, digits and underscore")
* @JMS\Type("string")
* @JMS\SerializedName("name")
*/
public $name;
...
No matter if the DTO has a complex structure and several nested DTOs... This validation works exactly the same way as shown above saving you tons of hours on implement data validation.
More articles on this topic:
- How to serialize nested Dtos in PHP Symfony 6+
- How to deserialize and validate nested Dtos in Symfony 6+
* Read more on the validator component