FlattenException & Error Status Codes

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

The job of this ErrorController is to turn the Exception that was thrown into a Response. By the way, the error_controller is actually configurable. So if you want to control the error response on your site, you have two options so far. First, register a listener to kernel.exception. Or second, override the error controller via the framework.error_controller config.

But... if you did that, you would be responsible for rendering both the normal exception pages and production error pages. If you want to change how an error page looks, there are better ways. We'll see.

Inside the __invoke() method, the ErrorController... is lazy! It immediately offloads the work to someone else - something called the errorRenderer. That returns some sort of exception... which apparently has getAsString(), getStatusCode() and getHeaders() methods. It uses these to create & return the Response.

The SerializerErrorRenderer

Let's... find out what this errorRenderer thing is: dd($this->errorRenderer).

... lines 1 - 25
class ErrorController
{
... lines 28 - 38
public function __invoke(\Throwable $exception): Response
{
dd($this->errorRenderer);
... lines 42 - 44
}
... lines 46 - 62
}

Move over and refresh. Ok cool: it's something called SerializerErrorRenderer. And actually, it only uses this class because this project has the serializer component installed. If you did not, this would be a different class - one that we'll see in a few minutes. And, by the way, this whole "error renderer" thing is part of a Symfony component called error-handler that's new in Symfony 4.4.

Let dig in! I'll close a class, then hit Shift + Shift to open SerializerErrorRenderer.php. Perfect!

The All-Important FlattenException

ErrorController calls this render() method, which immediately calls FlattenException::createFromThrowable. A FlattenException is basically a visual representation of an exception. And notice: the render() method returns a FlattenException.

Hold Command or Ctrl to jump into this class. Yea, see: it's not actually an exception - it doesn't extend Exception or implement Throwable. But it does contain a lot of the same info, like the exception $message, $code, $previous and the stack trace.

The FlattenException::createFromThrowable - if we jump to that - is a way to easily create this "visual representation" based on a real exception. And this contains some pretty important stuff. For example, if $exception is an instance of HttpExceptionInterface, then it calls $exception->getStatusCode() to get the status code and $exception->getHeaders() to get the headers. Both the status code and headers are ultimately stored on this FlattenException object and used by ErrorController when it creates the final Response.

Why do Some Exceptions Cause Different Status Codes?

So... what is this HttpExceptionInterface thing? We've actually seen it. Go back to ArticleController. We know that $this->createNotFoundException() is a shortcut to instantiate a new NotFoundHttpException. Click to open that class... and click again to open its base class HttpException. Here it is: HttpException implements HttpExceptionInterface.

This is a long way of showing you that certain exception classes in Symfony - like NotFoundHttpException - map to a specific status code. This works because they implement HttpExceptionInterface and because FlattenException uses this.

Why does NotFoundHttpException specifically map to a 404. It calls parent::__construct() with 404... that is set to a $statusCode property... and then returned from getStatusCode(). You can also pass custom $headers to the exception.

And there are a bunch of other exception classes like this. I'll double-click on the Exception directory at the top of PhpStorm. Wow! There are more than 15 in this directory alone, like BadRequestsHttpException, which will give you a 400 status code, PreconditionFailedHttpException, which will be a 412 and many more. Hmm, where's the IAmATeaPotHttpException?

If you throw any of these exceptions from anywhere in your app, they will trigger an error page with the correct status code. This is a powerful thing to understand.

Back in FlattenException, there is also another type of exception interface called RequestExceptionInterface. It's not as important and it always maps to a 400 status code.

If the exception doesn't implement either of these interfaces, the status code will be 500.

These are the most important parts of the FlattenException. Close it and go back to SerializerErrorRenderer. The job of this method is to create a FlattenException object from the exception and make sure it contains three things that the ErrorController needs: the status code, headers and a string representation of the error, which will become the body of the response. We've got the status code & headers... but we still need to somehow generate a "string" representation of this exception. Let's see how that's done next.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "doctrine/doctrine-bundle": "^2.0", // 2.0.7
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.1
        "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.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.1.1
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.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.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.2
        "oneup/flysystem-bundle": "^3.0", // 3.4.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.4
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "5.0.*", // v5.0.4
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/mailer": "5.0.*", // v5.0.4
        "symfony/messenger": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.4
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.3
        "symfony/yaml": "5.0.*", // v5.0.4
        "twig/cssinliner-extra": "^2.12", // v2.12.5
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.9.1
        "symfony/browser-kit": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.4
    }
}