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

Modeling the Error: ApiProblem Class

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 $10.00

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

Login Subscribe

Ok, 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.

The ApiProblem Class

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
}

Using ApiProblem

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.

Leave a comment!

7
Login or Register to join the conversation
Default user avatar
Default user avatar Majkell | posted 4 years ago

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. "

Reply

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!

Reply
Default user avatar

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);
}

Reply
Default user avatar

fixed it. thanks

Reply
Agnes Avatar

Um, I don't see this class in the resources directory. :( Only ApiTestCase and ResponseAsserter

Reply

Ah, you're right! I've just fixed this - try downloading the code again. Thanks for letting me know!

Reply
Default user avatar

Loving it :)

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
    }
}
userVoice