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 SubscribeOk, we've got a format for errors - and we're going to use this whenever anything goes wrong - like a 404 error, authentication error, 500 error, whatever. And each time, this format needs to be perfectly consistent.
So instead of creating this $data
array by hand when things go wrong, let's create a class that models all this stuff.
I actually started this for us. In PhpStorm, I'll switch my view back so I can see the resources/
directory at the root. Copy the ApiProblem.php
file. In AppBundle
, create a new Api
directory and paste the file here:
... lines 1 - 2 | |
namespace AppBundle\Api; | |
/** | |
* A wrapper for holding data to be used for a application/problem+json response | |
*/ | |
class ApiProblem | |
{ | |
private $statusCode; | |
private $type; | |
private $title; | |
private $extraData = array(); | |
public function __construct($statusCode, $type, $title) | |
{ | |
$this->statusCode = $statusCode; | |
$this->type = $type; | |
$this->title = $title; | |
} | |
... lines 24 - 45 | |
} |
The namespace is already AppBundle\Api
- so that's perfect. This holds data for an application/problem+json
response. It has properties for type
, title
and statusCode
- these being the three main fields from the spec.
And it also has a spot for extra fields:
... lines 1 - 7 | |
class ApiProblem | |
{ | |
... lines 10 - 15 | |
private $extraData = array(); | |
... lines 17 - 36 | |
public function set($name, $value) | |
{ | |
$this->extraData[$name] = $value; | |
} | |
... lines 41 - 45 | |
} |
If you call set()
, we can add any extra stuff, like the errors
key for validation. And when we're all done, we'll call the toArray()
method to get all this back as a flat, associative array:
... lines 1 - 7 | |
class ApiProblem | |
{ | |
... lines 10 - 24 | |
public function toArray() | |
{ | |
return array_merge( | |
$this->extraData, | |
array( | |
'status' => $this->statusCode, | |
'type' => $this->type, | |
'title' => $this->title, | |
) | |
); | |
} | |
... lines 36 - 45 | |
} |
Let's use this back in ProgrammerController
. Start with $apiProblem = new ApiProblem()
. The status code is 400, the type is validation_error
and the title is There was a validation error
. Let's knock this onto multiple lines for readability:
... lines 1 - 16 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 19 - 166 | |
private function createValidationErrorResponse(FormInterface $form) | |
{ | |
... lines 169 - 170 | |
$apiProblem = new ApiProblem( | |
400, | |
'validation_error', | |
'There was a validation error' | |
); | |
... lines 176 - 181 | |
} | |
} |
Get rid of the $data
variable. To add the extra errors
field, call $apiProblem->set()
and pass it the errors
string and the $errors
variable:
... lines 1 - 166 | |
private function createValidationErrorResponse(FormInterface $form) | |
{ | |
... lines 169 - 170 | |
$apiProblem = new ApiProblem( | |
400, | |
'validation_error', | |
'There was a validation error' | |
); | |
$apiProblem->set('errors', $errors); | |
... lines 177 - 181 | |
} | |
... lines 183 - 184 |
The last step is to update JsonResponse
. Instead of $data
, use $apiProblem->toArray()
. And to avoid duplication, use $apiProblem->getStatusCode()
instead of 400:
... lines 1 - 166 | |
private function createValidationErrorResponse(FormInterface $form) | |
{ | |
... lines 169 - 170 | |
$apiProblem = new ApiProblem( | |
400, | |
'validation_error', | |
'There was a validation error' | |
); | |
$apiProblem->set('errors', $errors); | |
$response = new JsonResponse($apiProblem->toArray(), $apiProblem->getStatusCode()); | |
... lines 179 - 180 | |
return $response; | |
} | |
... lines 183 - 184 |
It's not perfect yet - but this is a lot more dependable. Nothing should have change - so try the tests:
./bin/phpunit -c app --filter testValidationErrors
And yep! We're still green.
But go back and make the test fail somehow - like change the assert for the header. I want to see the response for myself. Re-run things:
./bin/phpunit -c app --filter testValidationErrors
Scroll up to the dumped response. Yes - we've got the Content-Type
header, the type
and title
keys, and a new status
field that the spec recommends.
Fix that test. Ok, now we're ready for other stuff to go wrong.
Hey @Majkell!
Hmm. So I know what's happening here, but I don't exactly know the cause :). This is an error because Symfony is trying to "autowire" your ApiProblem class. This means that either:
(A) You've added an ApiProblem argument to a constructor of a service somewhere or
(B) You've added an ApiProblem argument to one of your controllers.
If you can find that code, that's your problem :). In both cases, having an argument there doesn't make sense: you don't want Symfony to instantiate an ApiProblem for you - it's a simple "model" object that you should instantiate yourself when you need it.
Let me know if that makes sense and if you can find the bad code!
Cheers!
I ve used as an argument in the JwtTokenAuthenticator
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$apiProblem = new ApiProblem(401);
$apiProblem->set('detail', $exception->getMessageKey());
return $this->responseFactory->createResponse($apiProblem);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
}
public function supportsRememberMe()
{
return false;
}
public function start(Request $request, AuthenticationException $authException = null)
{
$apiProblem = new ApiProblem(401);
$message = $authException ? $authException->getMessageKey() : 'Missing credentials';
$apiProblem->set('detail', $message);
return new JsonResponse($apiProblem->toArray(), 401);
}
Um, I don't see this class in the resources directory. :( Only ApiTestCase and ResponseAsserter
Ah, you're right! I've just fixed this - try downloading the code again. Thanks for letting me know!
// 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
}
}
While I try to run the server this error shows: "Cannot autowire service "App\Controller\Api\ApiProblem": argument "$statusCode" of method "__construct()" has no type-hint, you should configure its value explicitly. "