Description
Digest (TL;DR)
A small service that allows tagged controllers to handle the exception(s) they registered for, optionally bubbling via inheritance to the closest match in the inheritance chain.
To replace, but incorporate, the current inflexible twig controller.
For my reasoning, explanations, and implementation details read the long story.
Code can be found here: https://gist.github.com/NinoFloris/92523a34cdf17a084d4d
Would I do good to integrate this into the framework and issue a pull request?
Long story
I have written a small class because I was fed up with the current state of afairs around event based exception handling (kernel event exception).
I like to fire my exceptions from all over the place and let them be handled by an exception listener.
This eases my boilerplate code by a huge amount, e.g. not having to check for the request format (which response would the request like to receive, html, json, xml). Talking about that, see #10538.
I then don't have to repeat myself over and over again with this identical logic. The exception listener/controller will do this all for me.
The problem
My problem with the way it is configured currently is that it isn't flexible. When I want to create some specific behavior for my app exceptions (all, say, inheriting from MyAppException
) With the current setup I have 3 options.
- Override the view templates of the twig exception controller
- Override the twig exception controller method (
config.yml
) - Roll my own exception listener and fire a subrequest to my own exception controller.
The downsides of all these options are:
- I cannot specify an exception design template per bundle. (e.g. error in the FrontendBundle should be able to look different than in the AdminBundle) And I cannot differentiate based on exception class but only on the given exception code.
- When I override the twig exception controller I don't have a lot of flexibility, the findtemplate function still handles the search the same so I now need to roll my own templatefinder. I also need to duplicate the functionality the original controller had because otherwise I would be degrading the UX for certain exceptions (e.g. checking for http statuscodes comes to mind).
- Currently the best option but also immediately the one that requires the most configuration and time.
These options are all not ideal and especially option 3 is very time consuming, but currently the only way to go.
Solution
I created an exception listener that almost exactly mirrors the one currently used in the httpkernel HttpKernel\Exception\Exceptionlistener
. It even uses the same signature to call the controller method.
On top of this I created a class (naming is open for discussion) ExceptionControllers
This class is a tag handler class for the tag myapp_service.exception_controller
This tag has 1 mandatory and 3 optional attributes
exception
= the exception you want to handle with this exception controllermethod
= the controller methodname that gets called (defaults to "showAction")match
= exact or inherited, does the controller only match exceptions with the exact classname or does it allow children of its handled exception. (defaults to "inherited")bundle-scope
= the site/origin of the thrown exception you want to handle e.g. only handleRuntimeException
if it originated from the MyAppFrontBundle (defaults to "" which is global)
This ExceptionControllers
class is constructor injected into the exception listener which can then 'query' the class for a correct handler.
The priority for handling is narrow, bundle, scope first. Closest match in the inheritance chain is preferred. When a candidate controller set match="exact"
the inheritance is n=1
(just itself).
Finally it is up to the controller to issue the correct response and do (additional) logging.
About BC
This solution keeps BC because we could still allow the twig controller to be overridden via the config.yml. What needs to change is the internal handling for the HttpKernel\Exception\Exceptionlistener
class which would then get the ExceptionControllers
class injected, replacing the "controller" string.
The twig exception controller would then have to be tagged like so (tag name would then be changed of course)
<tag name="myapp_service.exception_controller" exception="Exception" />
Where match="inherited"
, method="showAction"
and bundle-scope=""
are implied through defaults.
This will bubble all the unhandled exceptions to this controller if there is no controller closer to the exception in the inheritance chain or a controller in a narrower scope. That is all :D
Examples
<tag name="myapp_service.exception_controller" exception="MyApp\ServiceBundle\Exception\MyAppException" match="exact" />
<tag name="myapp_service.exception_controller" exception="MyApp\ServiceBundle\Exception\MyAppException" match="inheritance" bundle-scope="MyAppFrontBundle" />
Future ideas
Let the ExceptionListener
fall back to the next best controller to handle the exception. If the previous controller did not produce a response or threw an error. However we should limit this to trying a maximum amount of controllers to prevent the final response from possibly taking waaay too long.
BC breaking changes
- For the best experience the
ExceptionListener
should refrain from logging and let the controllers handle that. I use an abstract classBaseExceptionController
with thelogException
method from the currentExceptionListener
in it to very easily do the logging theExceptionListener
did.
Possible fix is to only log if the controller is in some way related to the twig controller approach and is therefore using "the old way" (need to be smart about overridden twig controllers). - The dependency
ExceptionControllers
has onKernel
.
It could easily be made optional through property injection, or attributeon-invalid="null"
and shutting off thebundle-scope
functionality if the Kernel is not defined.