Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Validation

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

There are a bunch of different ways for the users of our API to mess things up, like bad JSON or doing silly things like passing a negative number for the value field. This is dragon gold, not dragon debt!

Invalid JSON

This chapter is all about handling these bad things in a graceful way. Try the POST endpoint. Let's send some invalid JSON. Hit Execute. Awesome! A 400 error! That's what we want. 400 - or any status code that starts with 4 - means that the client - the user of the API - made a mistake. 400 specifically means "bad request".

In the response, the type is hydra:error and it says: An error occurred and Syntax Error. Oh, and this trace only shows in the debug environment: it won't be shown on production.

So this is pretty sweet! Invalid JSON is handled out-of-the-box.

Business Rules Validation Constraints

Let's try something different, like sending empty JSON. This gives us the dreaded 500 error. Boo. Internally, API platform creates a DragonTreasure object... but doesn't set any data on it. And then it explodes when it hits the database because some of the columns are null.

And, we expected this! We're missing validation. Adding validation to our API is exactly like adding validation anywhere in Symfony. For example, find the name property. We need name to be required. So, add the NotBlank constraint, and hit tab. Oh, but I'm going to go find the NotBlank use statement... and change this to Assert. That's optional... but it's the way the cool kids tend do it in Symfony. Now say Assert\NotBlank:

... lines 1 - 19
use Symfony\Component\Validator\Constraints as Assert;
... lines 21 - 51
class DragonTreasure
{
... lines 54 - 61
#[Assert\NotBlank]
... line 63
private ?string $name = null;
... lines 65 - 188
}

Below, add one more: Length. Let's say that the name should be at least two characters, max 50 characters... and add a maxMessage: Describe your loot in 50 chars or less:

... lines 1 - 19
use Symfony\Component\Validator\Constraints as Assert;
... lines 21 - 51
class DragonTreasure
{
... lines 54 - 61
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 50, maxMessage: 'Describe your loot in 50 chars or less')]
private ?string $name = null;
... lines 65 - 188
}

How Errors Look in the Response

Good start! Let's try it again. Take that same empty JSON, hit Execute, and yes! A 422 response! This is a really common response code that usually means there was a validation error. And behold! The @type is ConstraintViolationList. This is a special JSON-LD type added by API Platform. Earlier, we saw this documented in the JSON-LD documentation.

Watch: go to /api/docs.jsonld and search for a ConstraintViolation. There it is! API Platform adds two classes - ConstraintViolation and ConstraintViolationList to describe how validation errors will look. A ConstraintViolationList is basically just a collection of ConstraintViolations... and then it describes what the ConstraintViolation properties are.

We can see these over here: we have a violations property with propertyPath and then the message below.

Adding More Constraints

Ok! Let's sneak in a few more constraints. Add NotBlank above description... and GreaterThanOrEqual to 0 above value to avoid negatives. Finally, for coolFactor use GreaterThanOrEqual to 0 and also LessThanOrEqual to 10. So something between 0 and 10:

... lines 1 - 51
class DragonTreasure
{
... lines 54 - 68
#[Assert\NotBlank]
private ?string $description = null;
... lines 71 - 77
#[Assert\GreaterThanOrEqual(0)]
private ?int $value = null;
... lines 80 - 82
#[Assert\GreaterThanOrEqual(0)]
#[Assert\LessThanOrEqual(10)]
private ?int $coolFactor = null;
... lines 86 - 192
}

And while we're here, we don't need to do this, but I'm going to initialize $value to 0 and $coolFactor to 0. This makes both of those not required in the API: if the user doesn't send them, they'll default to 0:

... lines 1 - 51
class DragonTreasure
{
... lines 54 - 68
#[Assert\NotBlank]
private ?string $description = null;
... lines 71 - 77
#[Assert\GreaterThanOrEqual(0)]
private ?int $value = 0;
... lines 80 - 82
#[Assert\GreaterThanOrEqual(0)]
#[Assert\LessThanOrEqual(10)]
private ?int $coolFactor = 0;
... lines 86 - 192
}

Ok, go back and try that same endpoint. Look at that beautiful validation! Also try setting coolFactor to 11. Yup! No treasure is that cool... well, unless it's a giant plate of nachos.

Passing Bad Types

Ok, there's one last way that a user can send bad stuff: by passing the wrong type. So coolFactor: 11 will fail our validation rules. But what if we pass a string instead? Yikes! Hit Execute. Okay: a 400 status code, that's good. Though, it's not a validation error, it has a different type. But it does tell the user what happened:

the type of the coolFactor attribute must be int, string given.

Good enough! This is thanks to the setCoolFactor() method. The system sees the int type and so it rejects the string with this error.

So the only thing that we need to worry about in our app is writing good code that properly uses type and adding validation constraints: the safety net that catches business rule violations... like value should be greater than 0 or description is required. API Platform handles the rest.

Next: our API only has one resource: DragonTreasure. Let's add a second resource - a User resource - so that we can link which user owns which treasure in the API.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice