If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHay un montón de formas diferentes en las que un cliente de la API puede enviar datos erróneos: puede enviar un JSON mal formado... o enviar un campo title
en blanco... quizás porque un usuario se olvidó de rellenar un campo en el frontend. El trabajo de nuestra API es responder a todas estas situaciones de forma informativa y coherente, de modo que los errores se puedan entender, analizar y comunicar fácilmente a los humanos.
Ésta es una de las áreas en las que la Plataforma API realmente destaca. Hagamos algunos experimentos: ¿qué ocurre si enviamos accidentalmente un JSON no válido? Elimina la última llave.
¡Pruébalo! ¡Woh! ¡Qué bien! Esto nos devuelve un nuevo "tipo" de recurso: un hydra:error
. Si un cliente de la API entiende a Hydra, sabrá al instante que esta respuesta contiene detalles del error. E incluso si alguien no ha oído hablar nunca de Hydra, ésta es una respuesta súper clara. Y, lo más importante, todos los errores tienen la misma estructura.
El código de estado también es 400 -lo que significa que el cliente ha cometido un error en la petición- y hydra:description
dice "Error de sintaxis". Sin hacer nada, la Plataforma API ya está gestionando este caso. Ah, y el trace
, aunque puede ser útil ahora mismo durante el desarrollo, no aparecerá en el entorno de producción.
¿Qué pasa si simplemente borramos todo y enviamos una petición vacía? Ah... eso sigue siendo técnicamente un JSON no válido. Prueba sólo con {}
.
Ah... esta vez obtenemos un error 500: la base de datos está explotando porque algunas de las columnas no pueden ser nulas. Ah, y como he mencionado antes, si utilizas Symfony 4.3, es posible que ya veas un error de validación en lugar de un error de base de datos debido a una nueva función en la que las reglas de validación se añaden automáticamente al leer las reglas de la base de datos de Doctrine.
Pero, tanto si ves un error 500, como si Symfony añade al menos una validación básica por ti, los datos de entrada permitidos son algo que queremos controlar: quiero decidir las reglas exactas para cada campo.
Tip
En realidad, la auto-validación no estaba habilitada por defecto en Symfony 4.3, pero puede que lo esté en Symfony 4.4.
Añadir reglas de validación es... oh, tan bonito. Y, a menos que seas nuevo en Symfony, esto te parecerá deliciosamente aburrido. Por encima de title
, para que sea obligatorio, añade@Assert\NotBlank()
. Añadamos también aquí @Assert\Length()
con, por ejemplo,min=2
y max=50
. Incluso pongamos en maxMessage
Describe tu queso en 50 caracteres o menos
... lines 1 - 14 | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 16 - 37 | |
class CheeseListing | |
{ | |
... lines 40 - 46 | |
/** | |
... lines 48 - 49 | |
* @Assert\NotBlank() | |
* @Assert\Length( | |
* min=2, | |
* max=50, | |
* maxMessage="Describe your cheese in 50 chars or less" | |
* ) | |
*/ | |
private $title; | |
... lines 58 - 175 | |
} |
¿Qué más? Por encima de description
, añade @Assert\NotBlank
. Y para el precio,@Assert\NotBlank()
. También podrías añadir una restricción GreaterThan
para asegurarte de que está por encima de cero.
... lines 1 - 37 | |
class CheeseListing | |
{ | |
... lines 40 - 58 | |
/** | |
... lines 60 - 61 | |
* @Assert\NotBlank() | |
*/ | |
private $description; | |
... line 65 | |
/** | |
... lines 67 - 70 | |
* @Assert\NotBlank() | |
*/ | |
private $price; | |
... lines 74 - 175 | |
} |
Bien, vuelve a cambiar y prueba a no enviar datos de nuevo. ¡Woh! ¡Es increíble! ¡El @type
es ConstraintViolationList
! ¡Es uno de los tipos descritos por nuestra documentación JSON-LD!
Ve a /api/docs.jsonld
. Debajo de supportedClasses
, está EntryPoint
y aquí están ConstraintViolation
y ConstraintViolationList
, que describen el aspecto de cada uno de estos tipos.
Y los datos de la respuesta son realmente útiles: una matriz violations
en la que cada error tiene un propertyPath
-para que sepamos de qué campo procede ese error- y message
. Así que... ¡todo funciona!
Y si intentas pasar un title
de más de 50 caracteres... y se ejecuta, ahí está nuestro mensaje personalizado.
¡Perfecto! ¡Ya hemos terminado! Pero espera... ¿no nos falta un poco de validación en el campoprice
? Tenemos @NotBlank
... pero ¿qué nos impide enviar el texto de este campo? ¿Algo?
¡Vamos a intentarlo! Establece el precio en apple
, y ejecuta.
¡Ja! ¡Falla con un código de estado 400! ¡Es increíble! Dice:
El tipo del atributo precio debe ser int, cadena dada
Si te fijas, está fallando durante el proceso de deserialización. Técnicamente no es un error de validación, es un error de serialización. Pero para el cliente de la API, parece casi lo mismo, excepto que esto devuelve un tipo de error en lugar de un ConstraintViolationList
... lo que probablemente tiene sentido: si algún JavaScript está haciendo esta petición, ese JavaScript probablemente debería tener algunas reglas de validación incorporadas para evitar que el usuario añada texto al campo del precio.
La cuestión es: La Plataforma API, bueno, en realidad, el serializador, conoce los tipos de tus campos y se asegurará de que no se pase nada insano. En realidad, sabe que el precio es un entero por dos fuentes: los metadatos de Doctrine @ORM\Column
sobre el campo y la sugerencia de tipo de argumento en setPrice()
.
De lo único que tenemos que preocuparnos realmente es de añadir la validación de "reglas de negocio": añadir las restricciones de validación de @Assert
para decir que este campo es obligatorio, que ese campo tiene una longitud mínima, etc. Básicamente, la validación en la Plataforma API funciona exactamente igual que la validación en cualquier aplicación Symfony. Y la Plataforma API se encarga del aburrido trabajo de asignar los fallos de serialización y validación a un código de estado 400 y a respuestas de error descriptivas y coherentes.
A continuación, ¡creemos un segundo Recurso API! ¡Un usuario! Porque las cosas se pondrán realmente interesantes cuando empecemos a crear relaciones entre recursos.
Hey sasa1007 !
Yea, there are a lot of ways to handle showing validation errors in Vue... because it's "sort of simple", but can involve a lot of repetition.
Should I create empty ErrorProduct for field Product
I'm not sure what you mean here - are these Vue components?
The easiest way to accomplish this might be to have a violations
data on Vue that you set after the AJAX call fails. Each field would conditionally render the violation if it's there (you could of course, avoid some repetition by creating a re-usable component). I think the problem that you're talking about is the fact that the violations
are an array ( [ ]
) instead of an object where each field name is a key in that object. That, indeed, makes it harder to handle in JavaScript (because you need to loop over the whole set to look for a specific field error).
Instead of changing this in Vue, I would use a helper function in JS to normalize however you want. For example:
const normalizedViolations = {};
response.data.violations.forEach((violation) => {
normalizedViolations[violation.propertyPath] = violation.message;
});
Then you should be good. You could put this in a module so that you could re-use it:
// normalize-violations.js
export default function(violations) {
// .. all the code above
return normalizedViolations;
}
API Platform us following a "spec" with their response... and I think it's just easier to do the normalizing in JS than try to hijack API Platform.
Let me know if this helps :).
Cheers!
One more thing how to handle this kind of errors?
I got this error that is not in violations array
@context: "/api/contexts/Error"
@type: "hydra:Error"
hydra:description: "The type of the "region" attribute must be "string", "integer" given."
hydra:title: "An error occurred"
and this is my asserts in entity
/**
* @Assert\NotNull(message="Region ne sme da bude prazno polje")
* @Assert\NotBlank(message="Region ne moze ostati prazno polje")
* @Assert\Length(max="50", maxMessage="Licenca moze imate maksimum {{value}} karaktera")
* @ORM\Column(type="string", length=50, nullable=true)
* @Groups({"region:read", "region:write", "aplication:read"})
*/
private $region;
Hey sasa1007 !
If I remember correctly, this type of error doesn't come from the @Assert\
annotations - it comes from the type-hints that you have on your setter function. My guess is that your setRegion
function has a string
type-hint on the argument (and maybe you also have declare(strict_types=1)
on your class... I can't remember if that's needed).
The point is: this error is because if your type-hint - API Platform is smart enough to read this and not allow an integer to be set on this field :).
Ideally, you would not get any of these types of errors (and instead would only get true validation errors) because these types of errors are impossible to "map" correctly to the field. But this is tricky due to how Symfony's validation system works: data first gets set onto your object and then it's validated. So, you could add a @Assert\Type("string")
to your property, but you would need to remove the string
type-hint from your argument... which (I admit) is kind of a bummer. Removing the declare(strict_types=1)
might also work (I can't remember), but again - if you like using strict types, that's a serious trade-off.
So, sorry I can't offer an exact solution - but hopefully this helps!
Cheers!
Hey julien_bonnier!
Yes, there are plenty of people who completely agree with what you said. If you create DTO's immediately, you get really nice, clean classes that model your API and can be "pure": they hold only API code and you can properly type your entity. I think the reason this isn't the *main* approach you see is that it requires more work. So, people tend to try to use their entities until it gets too complicated, and then they switch to a DTO. But going 100% DTO from the very beginning is a very robust approach.
Cheers!
Thank you so much @weaverryan !
this part of code is what I looking for
const normalizedViolations = {};
response.data.violations.forEach((violation) => {
normalizedViolations[violation.propertyPath] = violation.message;
});
Heythere !
I want to add a validation in a date field in a put and post operations. When I make the put request and this field doesn't exist in the payload I get a constraint error because this field must be string but this field is not required for this operation.
How can I solve this issue?
Thanks!
Raúl.
Hey Raul M.!
What does this constraint error look like exactly? And is your ApiResource class an entity?
If you're using NotBlank on the date field, then during a PUT operation, the date field (which was previously set when you created the item) will be loaded from the database and populated onto the object. Then, because it's missing from the JSON, it simply won't be changed. The end entity WILL then STILL have the date field populated. Validation is done on this final entity (where the date field is still populated), so it "should work". What's why I'm asking what that constraint error looks like, to make sure it IS coming from validation, and not potentially from something in the serializer. Also, what do your properties, groups and validation constraints look like?
Cheers!
Hi weaverryan ,
My ApiResource is an Entity and I finally solved this issue with the next configuration:
/**
* @var \DateTime
*
* @ORM\Column(name="fecha_inicio", type="date", nullable=false)
* @Assert\NotBlank(groups={"postValidation"})
* @Assert\Type("\DateTimeInterface")
* @Groups({
* "comisiones:read",
* "comisiones:write"
* })
*/
private $fechaInicio;
Is this correct?
Because when I change the configuration to:
/**
* @var \DateTime
*
* @ORM\Column(name="fecha_inicio", type="date", nullable=false)
* @Assert\NotBlank(groups={"postValidation"})
* @Assert\DateTime
* @Groups({
* "comisiones:read",
* "comisiones:write"
* })
*/
private $fechaInicio;
I get the next error:
{
"@context": "/api/contexts/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "fechaInicio: This value should be of type string.",
"violations": [
{
"propertyPath": "fechaInicio",
"message": "This value should be of type string.",
"code": null
}
]
}
Thanks in advance!
Raúl.
Hi again weaverryan !
Sorry but with the configuration I thought it worked I get also an error:
{
"@context": "/api/contexts/Error",
"@type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "DateTime::__construct(): Failed to parse time string (hola) at position 0 (h): The timezone could not be found in the database",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/appdata/www/vendor/symfony/serializer/Exception/NotNormalizableValueException.php",
"line": 31,
"args": []
},
...
How can I solve this validation?
Thank you for your help!
Raúl
Hey Raul M.!
Sorry for the slow reply!
First, the fact that using @Assert\DateTime
doesn't work is actually because this constraint is meant to be applied to a string: it asserts that a string has a valid "date time" format - https://symfony.com/doc/current/reference/constraints/DateTime.html - its name IS a little misleading. So using Type is correct.
However, I rarely (never?) use @Assert\Type. The reason relates to your second question/error: the one about "Failed to parse time string". Instead of using validation constraints to enforce type, I typically use type-hints on my setters / properties. This is probably what you have done also - you probably have something like this:
public function setFechaInicio(?\DateTimeInterface $fecha)
And this is ALSO what I would have. With this setup, having the @Assert\Type on the property is redundant: it is not possible for a non-DateTimeInterface to EVER be set onto this property. And so, that validation will 100% never fail. BUT, what happens if your user sends a string like "hola" for the "fechaInicio" field in the API? That's the 2nd error you're getting. In this case, the data cannot be "denormalized": the serializer cannot figure out how to create a DateTimeInterface object from that string. And so, you get this "non normalizable exception". This is expected, and it results in a 400 error. I use the term "type validation" for this - https://symfonycasts.com/screencast/api-platform-extending/type-validation
Let me know if that makes sense. From what I'm seeing, the 2nd error is expected (there would not be a big stacktrace in production) and the Assert\Type is redundant, but not causing problems.
Cheers!
Is there a way we can inject the business validations into the Symfony error object? this way the front end will have a single object to deal with in case of errors.
Hey @sridhar!
Sorry for the slow reply! Can you tell me more - I don’t quite understand what you mean by the “symfony error object”. Your business rules validation should be implemented via the validation system - using the @Assert annotations. And if you do this, those errors will be returned in the JSON. But... I have a feeling I’m not understanding you’re thinking fully, so let me know ;).
Cheers!
You have answered my question partially. I wanted to know if there was a way to inject the validation errors into symfony objects so that when the JSON is returned it contains the error message. It hadn't occured to me that we had @assert annotation in Symfony which does the job to an extent.
But then business application have several business rules, the simplest is to check if a candidate is old enough to apply for college or a job, in which case we need to derive it from the Date of Birth? Where should the code that computes the age be written? In the entity or the controller? Or do we have a service object?
Actual business scenarios can get a bit more complicated. There could be a series of vidations that the entity should meet before it can be actully written to the Database. Here is one usecase that come to mind. An individual should
1. Be over 18 years
2. Be a resident of the country
3. Have an Identity Proof
4. Have an address Proof......
Lets say the individual doesn't match any one of the above criteriaaI would like to trigger an error that is returned as a JSON. Wondering whats the right way to do it.
It appears that at as of Symfony 5 the @Assert\NotBlank does not require the parens for parameters, otherwise it will throw an error, therefore it is
* @Assert\NotBlank
and NOT
* @Assert\NotBlank()
Hey Jason O.!
Hmm. The way it's parsed is determined by the Doctrine annotations parser, and actually, both of these syntaxes should be valid (I usually always include the ()
, but I believe their optional). What error do you get when you try @Assert\NotBlank()
?
Cheers!
Hi,
When you say "Oh, and the trace, while maybe useful right now during development, will not show up in the production environment." are you saying we can't send error information to the client? I have a React client that is waiting for the API errors to display. How can I achieve that?
Also, thank you for the great videos!
Hey @Chris
On production you still will see the error message but it's just the stack trace part that won't be shown (You don't really want your users to see that information)
Cheers!
You would be more clear with "if you're using Symfony 4.3, you MIGHT already see a validation error instead of a database error", it could be confusing because it's not enabled by default.
Hey pcabreus
I'm sorry if that statement caused you some confusion. The automatic validation feature used to be enabled by default but Symfony people decided to change it. I believe it's enabled by default *only* on new projects
Cheers!
Hey MolloKhan and @Diego
Actually, this changed since the recording - and we'll need to add a note :). The feature does exist in Symfony 4.3, and can be enabled with some config. For about the first 2 weeks of June, if you started a new project, that configuration was present (via the recipe) and you *did* get auto validation. I was planning ahead for this. However, due to a few inflexibilities with the feature (which should be fixed for 4.4), we've disabled the feature in the browser. You can still enable it, but if you do nothing (as most people will), you will not get it.
We'll add a note to clarify that spot.
Cheers!
I had to add the following use statement to CheeseListing.php to get @Assert annotations to work:
use Symfony\Component\Validator\Constraints as Assert; // to use Symfony's built-in constraints
Was that left out or did I miss something?
Hey John,
Ah, we're sorry about that! Yes, namespace was missed in the first code block, it's fixed now: https://symfonycasts.com/sc...
Thank you for reporting it!
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.17.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}
On front side I am working with Vue js. So if we get array violations, what is best practice to write error mesage under every input field on front.
Should I create empty ErrorProduct for field Product,
then for loop over violatins array and based on propertyPath put message for that Product filed in ErrorProduct,
and then show error messages from ErrorProduct under the Product field in form?
It's quite complicated and has code that repeats itself, so I wonder if it's possible to get errors directly from the api platform so that the errors are separate for each field or something like that