© James Edgeworth 2018
Development Published 15 Feb 2017

'Monolithic' vs Per-Page Controllers

Controllers are a good starting point for developers to find and solve bugs. A visible problem on the website can be mapped to the controller action that handles it, which should quickly tell you what model-layer code is being involved, and in what order.

..Ok, there's a couple problems with that statement. Automated testing provides a much better method of finding bugs in an ideal world, but unfortunately its usage is not as widespread as it should be. It is also surprisingly common for controllers to end up bloated with model-layer code, which has numerous disadvantages. Whilst I can write an entire blog entry on this point alone, that's not the purpose of this entry - for now, I want to talk about approaches to laying out controllers to encourage good practices.

For sake of clarity, let's assume we are right at the start of a fresh new project, and that we are handling users.

 

Approach 1 - 'monolithic' controllers

  1. We create a "UserController", with an "indexAction". Let's allow this index route to be the user's account page.
  2. We need a "loginAction" - might as well put it in the UserController, as it relates to the user.
  3. We need the ability for someone to register. So let's add a "registerAction".
  4. If the user forgets their password, we need to handle that. So we add a "forgotPasswordAction".
  5. Now we need to handle the actual password change when they click a link in their email, so a "resetPasswordAction". Or should we handle this in the "forgotPasswordAction", looking out for a url parameter to say we are changing it? But we have a different form to fill in... ok, we will put it in it's own action.
  6. The user needs a profile page. Should we create a new controller just for this one action? Or should we put it in the "indexAction", as the controller is after all focusing on the user.. Speaking of which, the user control panel has many sub-pages and multiple forms. We can either split the actions across a UserController and a ProfileController. Either works.

You can see the decisions being made here. It starts with good intentions - the UserController is trying to look after user-related requests, but it ends up trying to do too much. It seems with this approach, the temptation to create 'helper' methods within this controller eventually becomes acceptable. The controller is trying far too hard to remain DRY, but a controller isn't meant to be handling business logic in itself. The assumption for navigating the code goes on the logic of "we are looking for an action that affects the User, so the action should be in the UserController". Makes enough sense, but quickly leads to very large controllers which tempt shortcuts.

 

Approach 2 - per-page controllers

  1. We create a "LoginController" which has one method - "indexAction". This is clearly the login page.
  2. We want the user to manage their account. We create an "AccountController". This is clearly the account page.
  3. We want the user to be able to register. We create a "RegisterController". This is clearly the registration page.
  4. We want the user to be able to reset their password. We create a "ForgotPasswordController". Again, it's intentions are obvious.
  5. And of course, handling the password reset. We create a "ResetPasswordController" which needs no introduction.

The decisions here make good sense. Each controller represents a 'page' of the site. By extension, no 'helper' methods are added to the controllers because their use is extremely limited, forcing the good practice of actual helper objects and libraries.

If a developer is to navigate this codebase, they can instantly see the decisions that went into the controller scheme. It is quick to see that each controller is representing a logical "page" of the project, and that if they needed to fix something on the login page, they know exactly which controller to open. The indexAction clearly handles the main purpose of the controller, and any other actions are good for ajax callbacks or any other requests needed by the corresponding page.

This approach removes any ambiguity of where to put controller-layer code. It requires more controllers than the 'monolithic' approach but provides much more focused, navigable code.

 

Routing comparison

Here's where Approach 1 begins to quickly feel wrong - as early as defining the routing. Note that items in italic are ajax requests:

Route Approach 1 Approach 2
/usercp UserController::indexAction() UserController:indexAction()
/register UserController::registerAction() RegisterController:indexAction()
/login UserController::loginAction() LoginController::indexAction()
/forgot-password UserController::forgotPasswordAction() ForgotPasswordController::indexAction()
/profile/{slug} UserController::profileAction($slug) ProfileController::indexAction($slug)
/profile/{slug}/hobbies UserController::profileHobbiesAction($slug) ProfileController::hobbiesAction($slug)
/cart CheckoutController::cartAction() CartController:indexAction()
/cart/update-qty/{id} CheckoutController::cartUpdateQuantityAction($id) CartController:updateQuantityAction($id)
/checkout CheckoutController::indexAction() CheckoutController:indexAction()
/checkout/update-shipping CheckoutController::updateShippingAction() CheckoutController:updateShippingAction()

The routes themselves make perfect sense, but Approach 1 bundles many arguably related actions into a small number of controllers. Add just a few more controllers and we start having to use guesswork to locate some actions or indeed which controller is handling the route's path. As controllers get larger, the problems become more blatant. The more the requirements increase, the more ambiguous the codebase becomes.

 

Conclusion

Both approaches will produce a codebase which works, but at different levels of maintainability and clarity. Approach 2 follows the principle of thin controllers, fat models, whereas approach 1 tends to become a situation where the controller is trying to be too helpful when it is merely supposed to be an usher between the HTTP layer, and the model and view layers.

It is perfectly fine to just have one method in a controller class if that's all that is required. If there are more methods/actions in a controller, it is then very safe to assume they are handling some other such request of that page (such as an ajax callback, or Paypal IPN). If you are using controllers in the monolithic sense, I encourage you to try laying them out per-page and seeing how much easier and sensible the code becomes.