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

Adding Battle 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

It doesn't make any sense to create a battle without a Programmer or a Project. But guess what - you can! Or at least, you kind of can: we don't have validation to prevent that yet!

The validation system we created in earlier courses is air-tight: as long as we add the constraint annotations, it just works. So normally, I might not write a test for failing validation. But I will now... because we're going to add a twist.

Testing for Validation

Add a new public function testPOSTBattleValidationErrors():

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 44
public function testPOSTBattleValidationErrors()
{
... lines 47 - 64
}
}

Copy the first bits from the previous function that create the data and make the request:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 44
public function testPOSTBattleValidationErrors()
{
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
$data = array(
'projectId' => null,
'programmerId' => $programmer->getId()
);
// 1) Create a programmer resource
$response = $this->client->post('/api/battles', [
'body' => json_encode($data),
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 62 - 64
}
}

But, don't actually create a project! Instead, send null for the projectId. Since starting a battle against nothing is nonsense, assert that 400 is the response status code. This follows the pattern we did before in ProgrammerControllerTest.

And actually, that test shows off the validation errors response format: there should be an errors key with field names for the errors below that. Each field could technically have multiple errors, so that's an array:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 211
public function testValidationErrors()
{
... lines 214 - 230
$this->asserter()->assertResponsePropertyExists($response, 'errors.nickname');
$this->asserter()->assertResponsePropertyEquals($response, 'errors.nickname[0]', 'Please enter a clever nickname');
... lines 233 - 234
}
... lines 236 - 288
}

Check for the error in our code with $this->asserter()->assertResponsePropertyExists(): the field should be errors.projectId:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 44
public function testPOSTBattleValidationErrors()
{
... lines 47 - 61
$this->assertEquals(400, $response->getStatusCode());
$this->asserter()->assertResponsePropertyExists($response, 'errors.projectId');
... line 64
}
}

Next, check for the exact message: assertResponsePropertyEquals() with errors.projectId[0] - so the first and only error - set to This value should not be blank.:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 44
public function testPOSTBattleValidationErrors()
{
... lines 47 - 61
$this->assertEquals(400, $response->getStatusCode());
$this->asserter()->assertResponsePropertyExists($response, 'errors.projectId');
$this->asserter()->assertResponsePropertyEquals($response, 'errors.projectId[0]', 'This value should not be blank.');
}
}

Why that message? That's the default message for Symfony's NotBlank constraint.

Before we code this up, copy the method name and run the test:

./vendor/bin/phpunit --filter testPOSTBattleValidationErrors

It explodes with a 500 error! This is what happens when you're lazy and forget to add validation: the BattleManager panics because there is no Project. We do not want 500 errors, they are not hipster.

Adding Basic Validation

We know how to fix this! Go to BattleModel. Remember, this is the class that's bound to the form: so the annotations should go here. First, add the use statement. Type use NotBlank, let it auto-complete, delete the last part and add the normal as Assert:

... lines 1 - 6
use Symfony\Component\Validator\Constraints as Assert;
... lines 8 - 40

That's my shortcut to get the use statement.

Now, above project, add @Assert\NotBlank(). Do the same above programmer: @Assert\NotBlank():

... lines 1 - 8
class BattleModel
{
/**
* @Assert\NotBlank()
*/
private $project;
/**
* @Assert\NotBlank()
*/
private $programmer;
... lines 20 - 39
}

Done! Now run the test:

./vendor/bin/phpunit --filter testPostBattleValidationErrors

We're awesome! Or are we... there's a deeper problem! What prevents an API client from starting a battle with a Programmer that they do not own? Right now - nothing, besides karma and trusting that humankind will do the right thing. Unfortunately, that doesn't usually pass a security audit. Let's be heros and fix this security hole!

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice