Blog

Different Controllers for Different Contexts

Placeholder Avatar
Sebastian Porto
April 9, 2014

When we first start with Rails we learn to create one controller for each resource, e.g. CommentsController to deal with comments. Our routes file might look like:

resources :comments

But soon things get more complicated, suddenly we might want to get all of the comments for a post or all the comments for a user. Initially, what we might attempt to do is add conditional logic in our CommentsController to deal with this (get the comments for a post when given a post, get the comments for a user when given a user).

What if we also want all the comments for the current user and all the comments for everyone? The controller quickly becomes convoluted, unmaintainable and difficult to test.

The solution is simple, use different controllers for different contexts.

To cater for the four use cases above we would have a routes file that looks like:

``` # route for all comments resources :comments

resources :posts do # route for comments for a particular post resources :comments, controller: ‘posts/comments’ end

resources :users do # route for comments for a particular user resources :comments, controller: ‘users/comments’ end

resource :me do # route for comments for the current user resources :comments, controller: ‘me/comments’ end ```

Now we have four controllers. Yes, there are more files, but this new structure is easier to test and reason about. Our controllers will be placed in a subfolder for each context e.g.

In app/controllers/users/comments_controller.rb

class Users::CommentsController < ApplicationController def index # retrieve the comments for the user params[:user_id] end end

Different Views for Different Contexts

Another benefit is that you might want to show different views depending on the context, for example posts/1/comments/2 and users/4/comments/2 would show the same comment (2) but in a totally different way. You would simply create views in different files and everything would fall into place:

app/views/posts/comments/show.html.erb app/views//users/comments/show.html.erb

Common Methods

But many times we don’t want different views and now we have many routes that may seem redundant like:

posts/1/comments/2 users/1/comments/2

We could fix this with partials but we don’t need to. We can simplify the routes by only having the ones we need for different context.

  • In the context of post we want to list and create comments.
  • In the context of user we only want to list comments.

The other actions show, edit, update and destroy could just go to the root comments controller. This is because this actions have all the information they need (the id of the record).

Our routes file would look like:

```ruby resources :comments

resources :posts do # only respond to the actions we care about # in this case list and create comments for a post resources :comments, only: [:index, :new, :create], controller: ‘posts/comments’ end

resources :users do # only respond to the actions we care about # in this case listing comments for a user resources :comments, only: [:index], controller: ‘users/comments end ```

Searching and Filtering

Searching and filtering should be treated differently, they are not contexts like post or user. You should consider searching and filtering as refinements, potentially all our controller above could implement searching and filtering. The most common way to do this is by passing these refinements in the query string:

Get all comments with text ‘sam’ and created before given time: /comments?q=sam&created[lt]=1Matenia Rossides7458593

Get all comments with text ‘sam’ and created before given time but only in the context of user 3: /users/3/comments?q=sam&created[lt]=1Matenia Rossides7458593

So in conclusion, separating your contexts into multiple controllers can help a lot with making your application easier to understand, maintain and grow.