If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
It's finally that time: to add a little bit of validation to our token.
This should start to feel easy because we did all of this before with our
programmer. Now let me remind you, in the PogrammerController
, to
validate things we call this validate()
function, which is something I
created for this project before we even started:
... lines 1 - 36 | |
public function newAction(Request $request) | |
... lines 38 - 43 | |
if ($errors = $this->validate($programmer)) { | |
$this->throwApiProblemValidationException($errors); | |
} | |
... lines 47 - 56 | |
} | |
... lines 58 - 159 |
But the way the validation works is that on our Programmer
class we have
these @Assert
things:
... lines 1 - 5 | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 7 - 11 | |
class Programmer | |
... lines 13 - 17 | |
/** | |
* @Assert\NotBlank(message="Please enter a clever nickname") | |
* @Serializer\Expose | |
*/ | |
public $nickname; | |
... lines 23 - 50 | |
} |
So when we call the validate()
function on our controller, it reads this
and makes sure the nickname isn't blank. It's as simple as that!
Now if that validate()
function returns an array that has at least one
error in it, then we'll call this throwApiProblemValidationException
function,
which is something that we created inside this controller. You can see it
further down inside this same file:
... lines 1 - 147 | |
private function throwApiProblemValidationException(array $errors) | |
{ | |
$apiProblem = new ApiProblem( | |
400, | |
ApiProblem::TYPE_VALIDATION_ERROR | |
); | |
$apiProblem->set('errors', $errors); | |
throw new ApiProblemException($apiProblem); | |
} | |
... lines 158 - 159 |
What does it do? No surprises, it creates a new ApiProblem
object, sets
the errors as a nice property on it then throws a new ApiProblemException
.
We can see this if we look inside the programmer.feature
class. I'll search
for 400 because our validation errors return a 400 status code:
... lines 1 - 24 | |
Scenario: Validation errors | |
... lines 26 - 33 | |
Then the response status code should be 400 | |
... lines 35 - 40 | |
And the "errors.nickname" property should exist | |
But the "errors.avatarNumber" property should not exist | |
And the "Content-Type" header should be "application/problem+json" | |
... lines 44 - 130 |
You can see this is an example of us testing our validation situation. We're
checking to see that there are nickname
and avatarNumber
properties on
errors
.
The ApiToken
class also has one of these not blank things on it:
... lines 1 - 4 | |
use Symfony\Component\Validator\Constraints as Assert; | |
class ApiToken | |
{ | |
... lines 9 - 14 | |
/** | |
* @Assert\NotBlank(message="Please add some notes about this token") | |
*/ | |
public $notes; | |
... lines 19 - 32 | |
} |
So, all we need to do in our controller is call these same methods. First,
let's move that throwApiProblemValidationException
into our BaseController
,
because that's going to be really handy. And of course we'll make it protected
so we can use it in the sub classes:
... lines 1 - 289 | |
protected function throwApiProblemValidationException(array $errors) | |
{ | |
$apiProblem = new ApiProblem( | |
400, | |
ApiProblem::TYPE_VALIDATION_ERROR | |
); | |
$apiProblem->set('errors', $errors); | |
throw new ApiProblemException($apiProblem); | |
} | |
... lines 300 - 301 |
Perfect!
Next, let's steal a little bit of code from our ProgrammerController
and
put that into our TokenController
. So once we're done updating our token
object, we'll just call the same function, pass it the token instead of the
programmer and throw that same error:
... lines 1 - 16 | |
public function newAction(Request $request) | |
... lines 18 - 25 | |
$errors = $this->validate($token); | |
if ($errors) { | |
$this->throwApiProblemValidationException($errors); | |
} | |
... lines 30 - 33 | |
} | |
... lines 35 - 36 |
Great, so this actually should all be setup. Of course what I forgot to do was write the scenario first, shame on me! Let's write the scenario to make sure this is in full operating order.
I'll copy most of the working version. Here, we won't pass any request body.
Fortunately we've made our decode function able to handle that. We know the
status code is going to be 400. We can check to see that the errors.notes
property will equal the message that is on the ApiToken
class. It will
be this message right here:
... lines 1 - 31 | |
Scenario: Creating a token without a note | |
Given there is a user "weaverryan" with password "test" | |
And I authenticate with user "weaverryan" and password "test" | |
When I request "POST /api/tokens" | |
Then the response status code should be 400 | |
And the "errors.notes" property should equal "Please add some notes about this token" |
Alright!
This starts on line 33, so let's run just this scenario:
php vendor/bin/behat features/api/token.feature:33
Oh no, and it actually passes! Instead of the 400 we want, it is giving us
the 201, which means that things are not failing validation. You can see
for the note it says default note
. If you look back in our TokenController
...
Ah ha! It's because I forgot to take off the default value. So now it's either
going to be set to whatever the note is or null
:
... lines 1 - 16 | |
public function newAction(Request $request) | |
{ | |
... lines 19 - 20 | |
$data = $this->decodeRequestBodyIntoParameters($request); | |
... lines 22 - 23 | |
$token->notes = $data->get('notes'); | |
... lines 25 - 33 | |
} | |
... lines 35 - 36 |
And if it's set to null
we should see our validation kick in. And we do,
perfect!
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"silex/silex": "~1.0", // v1.3.2
"symfony/twig-bridge": "~2.1", // v2.7.3
"symfony/security": "~2.4", // v2.7.3
"doctrine/dbal": "^2.5.4", // v2.5.4
"monolog/monolog": "~1.7.0", // 1.7.0
"symfony/validator": "~2.4", // v2.7.3
"symfony/expression-language": "~2.4", // v2.7.3
"jms/serializer": "~0.16", // 0.16.0
"willdurand/hateoas": "~2.3" // v2.3.0
},
"require-dev": {
"behat/mink": "~1.5", // v1.5.0
"behat/mink-goutte-driver": "~1.0.9", // v1.0.9
"behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
"behat/behat": "~2.5", // v2.5.5
"behat/mink-extension": "~1.2.0", // v1.2.0
"phpunit/phpunit": "~5.7.0", // 5.7.27
"guzzle/guzzle": "~3.7" // v3.9.3
}
}