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 SubscribeOur end goal is to make our API easy to use so if something goes wrong our clients can actually debug it without pulling their hair out or having to email us.
Whenever you throw an exception in PHP there is going to be a message, like "No programmer found for username":
... lines 1 - 18 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 21 - 54 | |
public function showAction($nickname) | |
{ | |
$programmer = $this->getDoctrine() | |
->getRepository('AppBundle:Programmer') | |
->findOneByNickname($nickname); | |
if (!$programmer) { | |
throw $this->createNotFoundException(sprintf( | |
'No programmer found with nickname "%s"', | |
$nickname | |
)); | |
} | |
... lines 67 - 70 | |
} | |
... lines 72 - 185 | |
} |
This message is usually just for us as developers. When we're in development mode we see this message, but our clients don't. But, sometimes this message is useful, like in this case. Having something in the response that says "No programmer found for username" would help me as a client know that I have the right URL but that nickname I'm trying to use is missing.
There are other cases where we don't want to show the exception message. For example, if our database credentials are incorrect and we're getting a 500 error, we don't want to tell our client "invalid database credentials" -- that is a detail to hide.
Back in the spec, do we have a field for this? It's not supposed to be title
, because that's supposed to be the same for every type
. We could always add our own but if you look there is something called "detail" which is a "human readable explanation specific to this occurence of the problem." That's perfect for our use case!
Back in ProgrammerControllerTest
let's look for this exact message, "No programmer found for username".
At the bottom we'll say $this->asserter()->assertResponsePropertyEquals()
we'll fill this in so that when there's a 404 there will be a detail
field and it should be set to "No programmer found for username fake" because that's what's in the URL:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 165 | |
public function test404Exception() | |
{ | |
$response = $this->client->get('/api/programmers/fake'); | |
... lines 169 - 173 | |
$this->asserter()->assertResponsePropertyEquals($response, 'detail', 'No programmer found with nickname "fake"'); | |
} | |
} |
And if we try this out in our terminal, it's failing nice:
./bin/phpunit -c app --filter test404Exception
There's no detail
property yet. But no worries, creating that is easy! It all happens inside of our ApiExceptionSubscriber
.
Very simply, we say $apiProblem->set()
since that allows us to put in new fields. And we'll pass that detail
and $e->getMessage()
:
... lines 1 - 12 | |
class ApiExceptionSubscriber implements EventSubscriberInterface | |
{ | |
public function onKernelException(GetResponseForExceptionEvent $event) | |
{ | |
... lines 17 - 34 | |
$apiProblem->set('detail', $e->getMessage()); | |
... lines 36 - 45 | |
} | |
... lines 47 - 53 | |
} |
And that should do it, but don't do that...because that will expose the exception message of every exception in our system which is definitely not what we want to do.
So there has to be some way for us to determine whether or not it is safe to show the message to the user. There are a number of different ways to do this, FOSRestBundle has some options where you can whitelist on a class by class basis. And that's something we could even do here with an if statement that looks for a set of classes that are safe.
Implementing your own interface is also an option! I'll do something simple here which may or may not work for you, so do think about your project critically when you choose how to do this.
I'll check to see if ($e instanceof HttpExceptionInterface)
:
... lines 1 - 14 | |
public function onKernelException(GetResponseForExceptionEvent $event) | |
{ | |
... lines 17 - 33 | |
if ($e instanceof HttpExceptionInterface) { | |
$apiProblem->set('detail', $e->getMessage()); | |
} | |
... lines 37 - 45 | |
} | |
... lines 47 - 55 |
This is used for 404 or 403 errors, so it's typically things that we are in control of.
And you can see here that our 404 error implements that interface which will allow it to be caught by that.
Head back to the terminal and test that guy out:
./bin/phpunit -c app --filter test404Exception
Beautiful!
Now we have the opportunity to be more helpful to our users whenever we have a 404 error. Or, if you're creating an ApiProblem
by hand, you can set the detail
field manually.
"Houston: no signs of life"
Start the conversation!
// 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
}
}