4 simple tips to avoid fat controllers with Symfony
Fat controllers, you know them: Those bloated controllers which grew up in size drastically during a project lifetime. It is so easy to create them but difficult to get rid of them after a project went live once. Controllers should be kept to a minimum as they should be exchangeable and without any business logic.
In this article I provide 4 simple tips to avoid fat controllers and which are easy to follow.
by Frank Stelzer*
Let me give you an example for a fat controller. I just prototyped this code and never executed it, but i hope you get the idea:
1. Do not start coding in controllers
This tip is quite obvious and sounds easy to realise. But experience has shown the very reverse. During crunch times or prototyping it is tempting to start coding directly in controllers and do pretty much everything there, this means calling lots of different services, sending mails, maybe calling other web services, form handling or any other logic.
In very short time requirements could be implemented this way but for every new line this code looses maintainability and testability.
So, if you have to implement a new feature do not create a controller for it in the first step. Just create a new Handler, Worker, Monkey class (or whatever you prefer naming it) and start completely from scratch. When you need a form, inject the FormFactory. When you need a repository, just inject it. Whenever you need another service, inject it.
According your dependencies you’ll recognize very soon when you have to split up your class. I personally break up a class when the dependecy count exceeds the limit of five, some people doing it even for three dependencies.
Defining and requesting those dependencies will slow down the productivity in the first minutes and for some developer this could be quite bothersome but in long term this will save the whole project a lot of time.
After you are done with this handler it’s time creating a new controller for it. The controller then does nothing more than calling this service and maybe assigning some parameters. The whole logic of this new feature is located outside of the controller and the controller itself contains no further logic and is only some lines big.
2. Use the single responsibility principle also for controller actions
Nowadays the single responsibility principle is well known and widespread. It is often used but not in controllers. Each controller action should only have one responsibility not more.
Take the updateAction method from the example. This method provides the edit view and in the case of a post request it tries to execute the update workflow. You may know kind of those methods from the symfony 1 auto generated module code or even from the Symfony2 documentation but personally i do not follow this approach to make my code cleaner and more secure.
The updateAction could be refactored in this way:
The update action is split up in two actions. One for editing in the case of a GET request and the real update action in the case of a POST request. This is a pretty easy step thanks to the Method annotation.
Each action is now responsible for one thing and easy to understand. The new code may look more bloated than before but after optimizing the complete controller it will look another way.
3. Extract your shared methods or avoid them at all
The third tip is about shared methods in controllers. For new controllers they should be avoided at all as the logic there should be located somewhere else but not within the controller.
However when an existing controller contains those shared methods they have to be extracted and moved to some new classes.
The loadModel method in the given code snippet is an example for some shared logic misleadingly included in a controller. This code could be located everywhere but the controller is the worst place for it. Reusage of this code is not possible outside of this controller and leads to code duplication in the case it is needed somewhere else.
Solving this problem depends on how much the according code is encapsulated with the rest of the controllers’ code.
First of all you should create a new service and just cut and paste the original method content. Afterwards you have to analyse the dependencies of this code and inject them. Defining this service and its dependencies in the container is the next part. Now this service is ready for usage in the shortend controller. For backwards compatibility and for minimizing the refactoring time this new service should be called in the original controller method, like in this example:.
I now this sounds easier than in reality but you should try it anyway. With some experience you get to know which code could be swapped out and from which one you should keep your hands off.
4. Go the test driven way
The swapped out code from the second tip is additionally also more testable and this leads me to the fourth tip.
Going the test driven way you are in the need to produce testable code quick and without pain. The test cases are mostly realised with unit tests and rarely or even not at all with functional tests. Functional tests are however the only chance to test controllers reasonable (i am ignoring the possibility to “unit” test controllers here). Consequently developers doing TDD are automatically writting code which is not located in controllers. When you don’t believe it or when you are not used to TDD then just try it. Write some first test case of some web page and try to implement code for it. For sure this code will not be located in a controller. Only in the very last step when this code should be executable from the web you’ll make it accessible in controllers with the help of services.
Summarising the goal should always be to write as less code as possible within a controller. When you have to work with existing fat controllers try to minimize them as soon as possible.
Taking the fat controller from the start a minimized version could look like this:
The complete business logic has been removed from the controller and is accessed with the help of services. Those new services could be easily tested and are ready for re-usage.
But maybe you have any further trick? For any other ideas or feedback feel free to drop me some lines in the comments.
* Frank Stelzer (@frastel) has been working with PHP since 2001 and fell in love with Symfony during his university studies more than 6 years ago.
After his degree he started to work in the eSports industry and developed for a high load platform. Focusing on performance, clean code, unit testing or other aspects of code quality he loves to review and discuss about code.
For increasing his support in the Symfony world Frank joined the SensioLabs Deutschland Team in 2011 where he is working as a software architect.
Feel free to contact if you’re interested to write your own guest post!