-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DX] Better exception handling (as opposed to the current twig controller) #11752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I think your idea is not bad at all. If I may have a say in this, I'd like to see this in an upcoming commit. |
(btw, this should be labelled as DX) |
@NinoFloris DX is Developer eXperience, it's the initiative of the Symfony community to improve the experience for first-time users. For more info, see http://symfony.com/blog/making-the-symfony-experience-exceptional |
Should I just create a pull request for this because I don't have the feeling the issues are as actively checked as the pull requests are. Especially by the guys who are to judge the usefulness of this to the framework. (@fabpot , @stof ) P.S. Not meaning to sound like an awful person, I get that you have enough to do ;) |
Since it seems like your idea at least has some merit, yes! If you make a PR, then it will be easier for everyone else too see and comment :). Thanks! |
There is nothing requiring to use a subrequest to handle the exception inn your listener. You can render the error page directly in the listener. |
@NinoFloris issues are checked. But I haven't reviewed all issues this weeked (I have a life outside Symfony) and this one was still in the review queue (my github mailbox). And @fabpot just came back from vacations, so his own queue is probably huge. And moving the logging from the ExceptionListener to the controller is a BC break. It means that anyone replacing the controller now needs to take care of the logging as well (which is precisely something meant to be avoided in the current setup). If you don't want the logging for your custom exceptions, use your own listener on the |
First of I apologize again for coming over like I did. I was not trying to say you didn't care because I see how many issues and PR's there are and I get that you have a life, weekend, and vacation too ;) About your first post, I like the way it abstracts the process to a controller which fits well into the mental model of a symfony beginner. I really had to dig into the framework to find what handled this twig controller functionality and immediately my own listener felt really crappy because of all the checks and functionality I didn't think of to implement. And then I still had nothing that worked well (this solution is iteration 3)... I explained Using listeners IMO is much more flexible. You have a really fair point. I don't object to that.
I think it is great that with this you can easily have bundle specifc handling (different template) and app wide handling which is seamlessly handled. TL;DRIf you are thinking, just use a switch statement or a listener for every case and set up the priority correctly. Alright. If this is integrated into the framework, the bundles can stay decoupled because they don't have to know which other listeners are registered and how they all relate to each other. They can let the one ExceptionListener do all the magic of figuring out what the best candidate for the job is, and at the same time get a well written listener for free. Without having to learn a whole lot more than the basics of controllers and copy paste service wiring. Win win. |
this also means it is impossible to match based on interfaces, making it even less flexible than I though
how do you scope exceptions to a bundle ? The stack trace of an exception can go through several bundles (maybe even all bundles in some crazy case), as well as lots of code which is not in any bundle (especially when you follow the practice of having the logic in standalone libraries and having bundles with only the Symfony glue in them) |
About the interfaces, good point, they are not that common but this solution should still work with them. Will add. (adding class_implements() should do the trick). The scoping. I could change If you really want something with the errors from that library you either target its classes and/or target the bundle where you use that libraries code while making sure to wrap it (and optionally nest the previous exception). I really though of it as a feature to help the dev's that make real bundles. That is why I could permit the choice to take the file location of the last exception in the trace i.e. $ex->getFile(). This should be enough to catch errors that originate from your own bundles. And if it is an error that slipped your code (no try catch) This will still usually be a bundle below that. If however it cannot find the bundle it will try to handle the exception globally. That is expected behavior. You have poked some holes in a couple of the choices I made and that is a good thing. I sincerely thank you for that because I did not look at it this way yet. I think they are 'easy' fixes and don't degrade the ease of use that a developer would experience. Full honesty, do you think it could have potential if fully thought through? |
IMO, anything changing the render of the exception based on the file in which it was The usage you have for your FrontException can already be achieved with the current feature in a more powerful way
This would then required defining in which order we navigate between parent classes and implemented interfaces though, to know which one win (omitting the interfaces was indeed solving this by having a single meaningful order). Controlling which controller wins in this case would probably be even more complex to understand than the priority-based system we have currently (which only requires being before the core listener in simple cases, which is done by default as the core registers with |
Yes it is absolutely a hack, I was very happy to see however that a Symfony Bundle has the explicit constraint it must end with 'Bundle'. This way I could at least parse the filepath in a standardized way. Which can then be extrapolated back to the bundle again. In the end the getFile gives you the last location where things start to get unhandled. So It is not totally wrong behavior. But I get it you would prefer to see the first location where things start to go wrong?
I always admitted to that. It's just still not as pretty as typing up a very stupid controller and binding it through the service container. |
Well, writing a simple listener or a simple controller is not that different. And if you want to register the exception controllers through the container, it means defining services for them, which starts making it harder |
Would you? My opinion is that the stack trace is useful for debugging purposes but that a final response should always build on the last data it received. And seeing how I implement
I think there is still a meaningful difference due to the amount of easy extra's a controller gets that a listener goes without. This coupled with the known mental image of what a controller is. And if you can take out the thinking about matching, inheritance, bubbling etcetera. This helps a lot.
The twig controller will still be there as a final catch-all exception controller, which means that the overrides of it's twig templates also still work. I think 2. is leagues ahead of 4.
That is not so much the case because the handler decides which interface it wants to handle. So it would still just be exact > inheritance. Each class in the thrown exception inheritance chain gets scanned for a match, the match was just a class name but it should now also match interfaces. The closer to the original class the better, so if a handler registers MyExceptionInterface and the thrown exception implements it then we are already there. If not move on to the parent class. I will try to flesh out the idea I have for interface priority vs parent class tomorrow. But luckily the use case for multiple interface matches is very rare:
I would almost say, so rare that for multiple interfaces I would do a 'pick the first match' approach. |
except that you cannot know whether the interface was implemented directly or through interface of the parent or of another interface (except by some complex Reflection stuff)
And having several ways to do the same thing is not good for DX (because we need to document all of them, and then it confuses the user).
The issue is that once we had such way to do thing, people will expect to get full flexibility on it, which will make it way more than 80 LOC (with an insane config to support enough cases), and a complex matching of the controller to use.
The controller gets lots of things because you use container-aware controllers with the base controller class. But all your proposal are talking about defining the controller as a service. and for a controller defined as service, making it container-aware is a very bad practice, meaning there is no difference here On a side note, using listeners will be more efficient, because using subrequests has some overhead. |
Speaking about config :D would be nice if people could do something like:
This would take away the complexity of knowing anything about services. I don't think having a couple of options from easy to hardest is damaging to DX, I think it is helpful if you can pick the right option for your needs. Yes listeners are more efficient but you could of course always call the controller function directly without subrequests and then pass the return value as a response via the listener. I have implemented interface matching. The new size is 175 LOC, not too bad. Even if it doesn't come into the framework, which I'm of course expecting by now, it was a really fun exercise. The class only uses class_implements, so no reflection stuff. But it still is complex however. Like so
In this case the resolver will attribute the interface's A to C to ClassA and won't assign ClassB the exact implement of InterfaceB, there is no way I can detect this with spl functions. It is a weird situation to begin with, the only time I see this happening if one doesn't know a parent already implemented this interface.
Other way around is ok though
About container-aware, you are totally correct, I forgot about that. I use an abstract service definition to inject these candidates through something like See https://gist.github.com/NinoFloris/92523a34cdf17a084d4d for the interface inheritance resolver Hehehe but alright. It was a lot of fun building this, and I hope I can contribute in some other way in the future. |
+1 for not making a subrequest in exceptionhandling. |
@NinoFloris why this is closed now ? |
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.config.yml
)The downsides of all these options are:
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 isn=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 theExceptionControllers
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"
andbundle-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
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).
ExceptionControllers
has onKernel
.It could easily be made optional through property injection, or attribute
on-invalid="null"
and shutting off thebundle-scope
functionality if the Kernel is not defined.The text was updated successfully, but these errors were encountered: