Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Critical kernel.exception Event Listeners

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

Back at the browser, refresh to get the normal not found page, click to open the profiler... and go into Events. Because this was a 404 page, the kernel.exception event was dispatched. The most important listener - and the one that eventually will render this page - is ErrorListener.

Let's see how it works! Hit Shift + Shift and open ErrorListener.php: get the one from http-kernel/, not console/. Look down here for the getSubscribedEvents() method. Interesting: it listens to KernelEvents::CONTROLLER_ARGUMENTS and it listens to KernelEvents::EXCEPTION twice. We won't look at the CONTROLLER_ARGUMENTS listener method - but if you want to look back at it after finishing the entire tutorial, it should make sense. What it does is minor, but interesting.

When the kernel.exception event is dispatched, logKernelException() will be called first and then, later, onKernelException(), because it has a -128 priority.

How Exceptions are Logged

Find logKernelException() up on top. Its job is simple: log that an exception was thrown. If you follow the logException() logic, you'll see that it logs at a different level based on the status code. We're going to talk more soon about how different exceptions get different status codes. But the important piece here is that all 500 status code exceptions log at the critical() level, and 400 status code exceptions log at error(). If you're like us, you've probably used this fact before in your Monolog config to send 500 error logs to somewhere where you can be notified, like a Slack channel.

The Error Controller

The other listener method is onKernelException(). This is what's responsible for rendering the error page: both the nice development error page and the boring, production error page. It has a priority of -128 because it will eventually set the Response on the event, which will stop event propagation. The low priority makes it easy to register other listeners before this happens. Heck, you could easily create a listener that replaces this one, by setting the Response itself... though, there are better ways to customize the error process.

Go find this method. Hmm. The first thing it does is reference some $this->controller property. Let's find out what that is. dd($this->controller), then spin over to your browser, make sure you're on a 404 page and refresh.

... lines 1 - 29
class ErrorListener implements EventSubscriberInterface
{
... lines 32 - 49
public function onKernelException(ExceptionEvent $event, string $eventName = null, EventDispatcherInterface $eventDispatcher = null)
{
... lines 52 - 55
dd($this->controller);
... lines 57 - 89
}
... lines 91 - 149
}

Interesting: it's a string: error_controller. Find your terminal and run:

php bin/console debug:container error_controller

Surprise! error_controller is the id of a service! And its job apparently is to:

Render error or exception pages from a given FlattenException

Ok, we don't know what a FlattenException is yet, but apparently this is a controller that's good at rendering error pages. Let's see what it looks like!

Hit Shift + Shift to open ErrorController.php. Ooooo. It has an __invoke() method! This is an invokable controller! We talked about those earlier when we were inside the controller resolver. Usually a controller will have the format ClassName::methodName. Well, we learned that this is really ServiceId::methodName.

Anyways, for an invokable controller - a controller class that has an __invoke() method - the syntax is simpler: just, ServiceId: no :: stuff. That is what's happening here.

How the ErrorController is Called

Ok cool, so Symfony is going to execute this error_controller as a controller... and it will render the page. But... how? You can't normally just call a controller directly... or at least, you shouldn't do this.

Back in ErrorListener, take out the dd(). The logic here is fascinating. It says $request = $this->duplicateRequest() and passes the $exception and $request objects. Let's jump down to that method. Apparently, the Request class has a duplicate() method on it, which does exactly what you think - it effectively clones the object.

But, it passes this $attributes value to the third argument. This says:

Please create an exact copy of this Request. When you do that, keep the same query parameters as the original, the same POST parameters as the original, but replace the original request attributes with this new array.

So... it's a clone, but with different request attributes. Most importantly, the new attributes have an _controller key set to that error_controller string.

Move back up to the onKernelException() method. We have a Request object that has an _controller request attribute. Here's the magic: $response = $event->getKernel()->handle($request).

Yea! It's calling the HttpKernel::handle() method! The same one that we use in index.php and the same one we've been studying. Inside of handling the original request, it's handling a second request and getting back the response. And notice that it mentions something called a "sub request". We'll talk more about that soon.

For now, this is just a super fancy way of calling the error_controller. Instead of executing it directly, it creates a Request with an _controller attribute and tells HttpKernel to handle it. Neato!

Next, let's jump into error_controller itself and find out exactly how Symfony renders an error page. Because, it's a smart process: it renders the exception page in dev, the error page in prod and even changes format - like rendering JSON - when requested.

Leave a comment!

12
Login or Register to join the conversation
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | posted 2 years ago

Can I get an explanation what exactly the "onControllerArguments" method does? There some familiar bits but I failed to get a whole picture of what it's trying to achieve.

Reply

Hey Volodymyr T.!

Ha! Good eye! I purposely didn't talk about it because it's not that important and it IS a bit confusing/complex. The flow is this:

A) Somewhere in the main request, an exception is thrown
B) This causes ErrorListener::onKernelException() to be called
C) Inside this method (via the private duplicateRequest() method) a new Request object is created and the exception is added as an exception request attribute.
D) The Request created above is handled via a sub-request. This starts the whole request-response flow process again. The job of this sub-request is to render the error page (via the error controller)
E) Before the error controller is executed, the onControllerArguments() method is called (this was also called on the main request before the controller, but it didn't do anything then.
F) onControllerArguments() first finds which position the $exception argument is in in the error controller (that's the $k variable). Because remember, at this point, the controller arguments HAVE been determined, and since there is a exception key in the request attribute, if there is an $exception argument in the controller, then those have been matched up.
G) The method THEN looks to see what the type-hint is on the $exception argument. If there is NONE or if it is type-hinted with FlattenException, then the method creates a FlattenException from the exception and uses that as the argument.

So that's a LONG way of saying that this code allows your error controller to have an $exception argument and choose between the real exception object and a FlattenException object via the type-hint. How crazy is that? I'm not sure the exact history behind this - it comes from the introduction of the ErrorRenderer component - but apparently there are reasons to sometimes want one class or the other.

Cheers!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | weaverryan | posted 2 years ago | edited

Thanks for the detailed explanation. It now totally became clear! I stumbled to grasp that there is a possibility for a custom error controller which might have something else in its list of arguments than the public function __invoke(\Throwable $exception): Response of a core "ErrorController::__invoke()" method.

Reply
Daniil G. Avatar
Daniil G. Avatar Daniil G. | posted 3 years ago

Hello! Thanks a lot for this course.

Could you please explain why $this->conttroller equals "error_controller"? Who provide this string?

Reply

Hey Daniil G.!

Excellent question! On a high level, the ErrorListener class is a service that is registered by Symfony. It's first argument to __construct is $controller. So, on a high level, when Symfony registers ErrorListener as a service, it sets the string error_controller as its first argument. You can see it right here: https://github.com/symfony/symfony/blob/75e71e3bbefffe1e67d80e842d66721b98f6a531/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml#L81

In reality, you'll notice that the first argument is a parameter - %kernel.error_controller%. This touches on the type of stuff that we'll talk about in our next deep dive tutorial, which will be all about the Symfony container. But basically, you can set the error controller string via this config:


framework:
    error_controller: 'some_other_controller::renderError'

This is not usually something that you set in your app, and it defaults to error_controller. Whatever value is set for this (your custom value or the default error_controller) is eventually set to that parameter - https://github.com/symfony/symfony/blob/75e71e3bbefffe1e67d80e842d66721b98f6a531/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L238 - which is passed as the first argument to ErrorListener, which sets it on the $this->controller property. How fun is that ;).

That's a quick and dirty explanation - let me know if it helps!

Cheers!

Reply
Daniil G. Avatar

Thanks! Good explanation.

Reply

Hi, was it the weekend?
In fact I was able to solve my problem, I explain!
In the BaseFixture class, there is the getRandomReference method. Just before the last line, I replaced this `$randomReferenceKey = $this->faker->randomElement($this->referencesIndex[$groupName]);`, by this `$randomReferenceKey = $this->faker->unique()->randomElement($this->referencesIndex[$groupName]); `

Reply

Hey Diarill

Your solution looks good, but does your issue related to course? or it was question about your own implementation? And does it related to this chapter? It's important because when you say that something not work as expected we should know is it our course code broken? or it's just your theoretical question.

Cheers!

Reply

Hi Vladimir Sadicov
no! my problem has nothing to do with the course or this video. But by following this project (The_Spacebar) from the start, I imagined that I could find myself in the situation where (for example in the case of online application submission for a position, we would not want a candidate to submit multiple times) a unique post for each account.

Reply

Hey Diarill

Sorry for late reply. Thanks for approve that course is ok. And as I said you solution is pretty good for it, probably I would recommend you to refactor it in for example getUniqueReference() and so on. It's really depends on your needs!

Cheers! and Enjoy the courses!

Reply

Hello,
When I imagine a simple senariot, I run into a problem. I assumed that a user should only publish one article, so I added the unique user_id constraint in the Article entity. But when I launch the fixtures, boom !!! "Integrity constraint violation: 1062 Duplicate entry ". What signature should I add to solve the problem.
Thanks in advance

Reply

Hey Diarill!

> When I imagine a simple senariot, I run into a problem. I assumed that a user should only publish one article, so I added the unique user_id constraint in the Article entity. But when I launch the fixtures, boom !!! "Integrity constraint violation: 1062 Duplicate entry ".

Hmm. If you only want a user to publish one article... but executing your fixtures creates this error, then it sounds like your fixtures are written incorrectly - they are assigning more than one article to a user. Is it possible that your comment is being displayed below the wrong article? This question seems unrelated... it actually kind of seems related to using Alice YAML fixtures... which is why I might be confused :). Let me know!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works well for Symfony 6!

What PHP libraries does this tutorial use?

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