Symfony 2: Why You Should Move Your Logic into a Service

In typical MVC applications, people end up storing business logic into either the controller or the model layer. The problem in this approach is that in either case, you introduce code bloat and dependencies that should not really exist. Controllers, which are essentially the observer pattern, act as a traffic light, taking requests and directing them to another point. Models themselves should focus on the data within their domain. But what happens when you get complex interactions between these forces? What should you do? This is where the MOVE paradigm comes into play.

The MOVE paradigm (model-operations-views-events) break down this structure by placing your operational logic in the “O” part and making controllers into simple event handlers. With regards to Symfony, the operations portion more or less is the service layer, a section of code which is used to handle global tasks. Moving your business level logic into your service layer allows you to handle complex interactions between models, repositories, other services, etc. without bloating up the code in areas like your controllers nor models.

However, you might lose some level of flexibility by moving everything into your service layer. For instance, forms or less complex tasks might not be necessary to place inside a service. A good example is a simple delete CRUD style operation. It might go overboard writing the equivalent of a business delegate by having the complete delete operation inside a service as these operations don’t require a lot of code to handle. Similarly, a CRUD 1-to-1 mapping create operation between a model and a form probably does not require a special service, especially if you use the scaffolding mechanism from Symfony.

But if the operation is fairly complex where you have numerous levels of decision making processes, looping and/or interactions between other services, you probably have found an excellent candidate for a service level task. Part of the problem here is that any method/function should only do one major task. Once you have more operations, you should create additional methods/functions for doing each part. In this manner, you’re breaking down the problem a la divide-and-conquer methodology of programming and it will make things much easier when it comes to unit testing.

For myself, this issue of unit testing made the situation very apparent to me in a recent application. I wanted to test out a controller action but was forced to mock up numerous repositories and objects. The level of complexity in my test case far exceeded the amount of work that really was necessary and it became frighteningly apparent that I had too much going on in a single controller action. The thing is that I really just want to test the interface of the controller and the route, not every level of logic in the application. In short, the unit had become bloated.

At any rate, this is a good way to think about how you want to structure your application. I think if I had started with the test, then I would’ve realized very quickly how bloated my action was becoming rather than learn at a later stage. In the end, you need to become more aware of your application’s structure ahead of time not later. It should not require a unit test to tell you how poorly your structure is.