Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Validation Errors Test

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Tip

In this course we're using Symfony 2, but starting in episode 4, we use Symfony 3. If you'd like to see the finished code for this tutorial in Symfony 3, download the code from episode 4 and check out the start directory!

Errors! A lot of things can go wrong - like 500 errors when your database server is on fire, 404 errors, validation errors, authentication errors and errors in judgement, like wearing a khaki shirt with khaki shorts, unless you're on Safari.

In your API, handling all of these errors correctly ends up taking some effort. So that's why we're devoting an entire episode on getting a beautiful, robust error system into your Symfony API.

First, we'll talk about what most people think of when you mention errors: validation errors! In episode 1, we created this cool ProgrammerControllerTest class where we can test all of our endpoints for creating a programmer, updating a programmer deleting a programmer, etc etc. We don't have validation on any of these endpoints yet.

Test for a Required Username

So let's add some: when we POST to create, we really need to make sure that the username isn't blank. That would be crazy! Copy testPOST(). Down at the bottom, paste that, and rename it to testValidationErrors(). Get rid of the nickname data field and most of the asserts:

... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
... lines 8 - 123
public function testValidationErrors()
{
$data = array(
'avatarNumber' => 2,
'tagLine' => 'I\'m from a test!'
);
// 1) Create a programmer resource
$response = $this->client->post('/api/programmers', [
'body' => json_encode($data)
]);
... lines 135 - 144
}
}

Validation Error Status Code: 400

Ok, design time! Writing the test is our time to think about how each endpoint should work. Since we're not sending a username, what status code should the endpoint return? Use 400:

... lines 1 - 123
public function testValidationErrors()
{
... lines 126 - 131
$response = $this->client->post('/api/programmers', [
'body' => json_encode($data)
]);
$this->assertEquals(400, $response->getStatusCode());
... lines 137 - 144
}
... lines 146 - 147

There are a few other status codes that you could use and we talk about them in our original REST series. But 400 is a solid choice.

Validation Errors Response Body

Next, what should the JSON content of our response hold? Let me suggest a format. Trust me for now - I'll explain why soon. We need to tell the client what went wrong - a validation error - and what the validation errors are.

Use $this->asserter()->assertResponsePropertiesExist() to assert that the response will have 3 properties. The first is type - a string error code - the second is title - a human description of what went wrong - and the third is errors - an array of all of the validation errors:

... lines 1 - 123
public function testValidationErrors()
{
... lines 126 - 131
$response = $this->client->post('/api/programmers', [
'body' => json_encode($data)
]);
... lines 135 - 136
$this->asserter()->assertResponsePropertiesExist($response, array(
'type',
'title',
'errors',
));
... lines 142 - 144
}
... lines 146 - 147

Don't forget to pass the $response as the first argument. Now, think about that errors array property. I'm thinking it'll be an associative array where the keys are the fields that have errors, and the values are those errors. Use the asserter again to say assertResponsePropertyExists() to assert that errors.nickname exists:

... lines 1 - 123
public function testValidationErrors()
{
... lines 126 - 141
$this->asserter()->assertResponsePropertyExists($response, 'errors.nickname');
... lines 143 - 144
}
... lines 146 - 147

Basically, we want to assert that there is some error on the nickname field, because it should be required. Actually, go one step further and assert the exact validation message. Use assertResponsePropertyEquals with $response as the first argument, errors.nickname[0] as the second and, for the third, a nice message, how about "Please enter a clever nickname":

... lines 1 - 123
public function testValidationErrors()
{
... lines 126 - 141
$this->asserter()->assertResponsePropertyExists($response, 'errors.nickname');
$this->asserter()->assertResponsePropertyEquals($response, 'errors.nickname[0]', 'Please enter a clever nickname');
... line 144
}
... lines 146 - 147

Why the [0] part? It won't be too common, but one field could have multiple errors, like a username that contains invalid characters and is too short. So each field will have an array of errors, and we're checking that the first is set to our clever message.

And since we are sending a valid avatarNumber, let's make sure that there is no error for it. Use assertResponsePropertyDoesNotExist() and pass it errors.avatarNumber:

... lines 1 - 123
public function testValidationErrors()
{
... lines 126 - 141
$this->asserter()->assertResponsePropertyExists($response, 'errors.nickname');
$this->asserter()->assertResponsePropertyEquals($response, 'errors.nickname[0]', 'Please enter a clever nickname');
$this->asserter()->assertResponsePropertyDoesNotExist($response, 'errors.avatarNumber');
}
... lines 146 - 147

Love it! We've just planned how we want our validation errors to work. That'll make coding this a lot easier.

Leave a comment!

4
Login or Register to join the conversation
Szymon Z. Avatar
Szymon Z. Avatar Szymon Z. | posted 2 years ago

Is this course of REST good for symfony 5? Should i learn it? Or avoid it?

Reply

Hey Simon,

We do recommend to start with API Platform course, it's easier to learn and understand, and has Symfony 5 support. You can find a few courses about it here:
https://symfonycasts.com/sc...
https://symfonycasts.com/sc...

For more deep advanced learning and understanding of lower level of it you can look at our REST courses. The packages we're using in the REST courses should be compatible with Symfony 5 already, but if you find a problem during following the course - please, let us know in comments below and we will try to help you!

I hope this helps!

Cheers!

Reply
Default user avatar
Default user avatar julien moulis | posted 4 years ago

Hi guys,
I'm using this wonderful tuto to build an app. I have a question about returning an error on a oneToMany relation. I have the error returned but with no field name, just with a 0 index and as a string. Have you got any idea?

Reply

Hey julien moulis!

Hmm. That's a great question :). Are you using a CollectionType? In the eyes of the form component, if you have 3 of these things, then they are added to the form as indexes (e.g. form.products[0], form.products[1] and form.products[2]). Then, the errors are attached at the same "nodes".

So, there *is* certainly a way to take the index (e.g. 0) and find the correct thing (e.g. Product) that it's referring to. What exactly do you want to accomplish? And what does your API endpoint look like? Because, for example, if your user is sending an array of Product objects (via JSON), then communicating the index (e.g. 0) *is* the best way to tell them which has a validation error. But I'm guessing your situation is probably not exactly this :).

Cheers!

Reply
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of REST and errors are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}