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 SubscribeThis method is called by ErrorController
and its job is to return a FlattenException
that contains the status code, headers and body that should be set on the final error Response
. The FlattenException::createFromThrowable
intelligently sets the status code and headers. But we still need to, somehow, figure out what content to send back, like a JSON error, or an HTML page that says: "Please send help!".
To do that, SerializerErrorRenderer
first tries to figure out what format the user wants - like HTML or JSON. The $this->format
property is actually a callback that points down here to this getPreferredFormat()
method. This is a fancy way of getting the request and calling $request->getPreferredFormat()
. And... hey! We know that method! I'll hit Shift + Shift and open Response.php
from HttpFoundation
. Search for prepare()
. This method is called by a listener to the kernel.response
event. It normalizes a few things... including setting the Content-Type
header if it hasn't already been set. To help with that, it calls $request->getPreferredFormat()
to try to figure out if the user wants HTML, JSON or something else. One of the ways it figures this out is by looking at the Accept
header on the request.
Back in SerializerErrorRenderer
, we're once again using $request->getPreferredFormat()
, which will return a simple string like html
or json
.
Up in render()
, this is pretty cool: it says:
Hey serializer! Can you try to serialize the
FlattenException
object into this format?
If the format
is html
, this will fail with a NotEncodableValueException
: the serializer doesn't handle HTML. We'll talk about that case in a minute. But if the format is json
, xml
or some other format that the serializer does support, this will convert the exception to that format!
We can see this. If we refresh the page... we see the big HTML exception - and we'll see the code that makes this soon. Copy the URL, find your terminal and use curl
to fetch that URL. But also pass a -H
flag to add a header: "Accept: application/json"
:
curl https://localhost:8000/news/foo -H "Accept: application/json"
This will change the "preferred format" on the request to json
. And... check it out! It's a 404 status code but in a JSON format! We can even use text/xml
to see this in XML.
curl https://localhost:8000/news/foo -H "Accept: text/xml"
How does this work? One of the normalizers in the serializer is called ProblemNormalizer
. I'll hit Shift + Shift to open it: ProblemNormalizer.php
.
If you don't know much about the serializer component, the important thing to know is that a normalizer is responsible for taking an object and converting it into an array of data. Thanks to the supportsNormalization()
method, this class is used when you try to normalize a FlattenException
object.
This normalizer creates a response format that follows an HTTP specification: it helps us return an official, standardized error response. It's pretty simple: it sets keys for type
, title
, status
and detail
. In $debug
mode, it also adds class
and trace
. Also, the detail
key in debug mode will be the exception message... but in production, it will be the "status text", which is a generic "Not Found" message... or something similar, based on the status code. That's done so that your exception messages don't "leak" to the public.
The normalize()
method is passed the $exception
, which is the FlattenException
. But if you look back at SerializerErrorRenderer
, it also passes the original exception as an exception
key on the $context
- that's the 3rd argument to normalize()
.
So this gives us a really nice error response body, without any work. If you wanted to change this data, you could do that by adding your own custom normalizer. We actually talk about this in our API Platform Security Tutorial. You could decorate the ProblemNormalizer
... and maybe just add or tweak some data or you could create an entirely new normalizer. Heck, you could use the $context
in supports
- you need to implement ContextAwareNormalizerInterface
to make that work - and make that new normalizer responsible for only normalizing FlattenException
classes for a specific, original exception. If you want to try that and have problems, let us know.
Ok, close that class up. Next, let's find out what happens if the format is not something that the serializer can handle. Like, HTML.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-iconv": "*",
"antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
"aws/aws-sdk-php": "^3.87", // 3.133.20
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.2.3
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
"doctrine/orm": "^2.5.11", // 2.8.2
"easycorp/easy-log-handler": "^1.0", // v1.0.9
"http-interop/http-factory-guzzle": "^1.0", // 1.0.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
"knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
"knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
"knplabs/knp-time-bundle": "^1.8", // v1.16.0
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"league/html-to-markdown": "^4.8", // 4.9.1
"liip/imagine-bundle": "^2.1", // 2.5.0
"oneup/flysystem-bundle": "^3.0", // 3.7.0
"php-http/guzzle6-adapter": "^2.0", // v2.0.2
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^5.1", // v5.6.1
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.9", // v1.17.5
"symfony/form": "5.0.*", // v5.0.11
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/mailer": "5.0.*", // v5.0.11
"symfony/messenger": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.5", // v3.6.0
"symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
"symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
"symfony/routing": "5.1.*", // v5.1.11
"symfony/security-bundle": "5.0.*", // v5.0.11
"symfony/sendgrid-mailer": "5.0.*", // v5.0.11
"symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
"symfony/twig-bundle": "5.0.*", // v5.0.11
"symfony/validator": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.4", // v1.11.1
"symfony/yaml": "5.0.*", // v5.0.11
"twig/cssinliner-extra": "^2.12", // v2.14.3
"twig/extensions": "^1.5", // v1.5.4
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/inky-extra": "^2.12", // v2.14.3
"twig/twig": "^2.12|^3.0" // v2.14.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
"fakerphp/faker": "^1.13", // v1.13.0
"symfony/browser-kit": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/maker-bundle": "^1.0", // v1.29.1
"symfony/phpunit-bridge": "5.0.*", // v5.0.11
"symfony/stopwatch": "^5.1", // v5.1.11
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/web-profiler-bundle": "^5.0" // v5.0.11
}
}
Just a helpful tip.. If you get a certificate error when sending the curl command from the command line, just add the
--insecure
flag like so:curl https://127.0.0.1:8000/news/foo -H "Accept: application/json" --insecure
There's probably a better way to set this in development, but this solved the problem for me.