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.