Well, we are in business for almost one year and we are happy to say that quite a lot of applications were already delivered. In each one of them, we were definitely learning and moving toward the Plataforma way of doing things.

So I want to talk the way we treat Rails controllers at Plataforma. With love, of course, but we are going to go deeper than that, specifically in three steps:

1) Using InheritedResources to dry up controllers
2) Splitting your controllers by scope with nested controllers
3) Creating acts_as_* and filters to contain common logic and configuration among controllers

Step 1: Using InheritedResources to dry up controllers

InheritedResources is a tool that allows drying up your controllers, by removing the common logic and taking care of stuff like relationships.

For instance, imagine that you have an Article model which belongs to user. If you want to show all the articles for an specific user, your routes definition and controller are just the following:

# config/routes.rb
map.resources :users do |u|
  u.resources :articles
end
 
# app/controllers/articles_controller.rb
class ArticlesController < InheritedResources::Base
  belongs_to :users
end

It supports nested and/or polymorphic relationships, I18n and other stuff like named scopes.

Almost all of our controllers inherit from InheritedResources::Base and the first step is as simple as that.

Step 2: Splitting your controllers by scope with nested controllers

Sometime ago, Matt Jankowski from Thoughtbot wrote an excellent blog post on how they deal with scoped controllers, like in the scenario above where we want to show all the articles which belongs to an specific user.

Matt tell us that instead of having an ArticlesController, we should have a Users::ArticlesController, so ArticlesController is available if you want to show all the articles in the application.

Since we read this post, we started to use this setup on our applications as well. However, InheritedResources does not play nice with it by default and let’s check why. Our ArticlesController should be renamed to Users::ArticlesController and be rewritten as:

class Users::ArticlesController < InheritedResources::Base
  belongs_to :users
end

The problem is, according to the namespace Users, InheritedResources appends to all named routes the prefix “users”. But since Articles belongs to :user, it also appends “user”. So the resource_url method will actually call “users_user_article_url”, which is not defined. The fix is simple:

class Users::ArticlesController < InheritedResources::Base
  defaults :route_prefix => nil
  belongs_to :users
end

Matt’s post contains a lot of tips about using nested controllers and it’s a must-read.

Step 3: Creating acts_as_* and filters to contain common logic and configuration among controllers

As your application grow, you will start to notice that your controllers will have a lot of configuration values and methods for specific scopes. Let’s consider that in our application an user can only see his articles and we are going to retrieve the user from session. So instead of having a route “/users/:user_id/articles/:id” like above, we should just have “/articles/:id” and our controller will be similar to:

class Users::ArticlesController < InheritedResources::Base
  before_filter :find_user_from_session
  layout "users"
  defaults :route_prefix => nil
 
  protected
 
  # Get the articles scoped to the current user
  def begin_of_association_chain
    @current_user
  end
 
  # Simple method to retrieve an user from session
  def find_user_from_session
    @current_user ||= User.find(session[:user_id])
  end
end

With time, we will notice that several controllers use this same pattern, and we need to refactor it. Our first thought would be move part of the logic to ApplicationController, but our ApplicationController will only grow and grow as you add roles (like Admin) to your application.

A better approach would be to move this specific logic to a controller named Users::ApplicationController inside “app/controllers/users/application_controller.rb”:

class Users::ApplicationController < ApplicationController
  before_filter :find_user_from_session
  layout "users"
 
  protected
 
  def begin_of_association_chain
    @current_user
  end
 
  def find_user_from_session
    @current_user ||= User.find(session[:user_id])
  end
end

Now we can inherit from it and get InheritedResources methods by calling inherit_resources in our controller:

class Users::ArticlesController < Users::ApplicationController
  inherit_resources # the same as inheriting from InheritedResources::Base
  defaults :route_prefix => nil
end

But we are not very happy with this pattern, because we still need to call defaults and set some configuration values (which can be quite a few depending on your application).

What we do instead is create some macros inside a module called Filters. So we create a file at “app/controllers/users/filters.rb” with the following:

module Users::Filters
  def acts_as_user(options={})
    before_filter :find_user_from_session
    layout "users"
    defaults options.reverse_merge(:route_prefix => nil)
    include ControllerMethods
  end
 
  module ControllerMethods
    protected
 
    def begin_of_association_chain
      @current_user
    end
 
    def find_user_from_session
      @current_user ||= User.find(session[:user_id])
    end
  end
end

And extend this module in our ApplicationController:

class ApplicationController < ActionController::Base
  extend Users::Filters
end

And we can use it simply as:

class Users::ArticlesController < InheritedResources::Base
  acts_as_user
end

The main advantage here is that we have more control of our controller configuration through a single interface. If we need to configure a singleton controller or ensure that most controllers have pagination, we can simply do:

module Users::Filters
  def acts_as_user(options={})
    before_filter :find_user_from_session
    layout "users"
    has_scope :paginate, :only => :index unless options.delete(:paginate) == false
    defaults options.reverse_merge(:route_prefix => nil)
    include ControllerMethods
  end

And invoke it as:

acts_as_user :paginate => false
acts_as_user :singleton => true
acts_as_user :paginate => false, :singleton => true

Wrapping up

As you add more roles to your application, you create more filter. Remember that the important here is to keep your code DRY and is NOT reduce the lines of code. So just place inside those acts_as_* helpers configuration which is common to almost all (if not all) controllers. There is some convention over configuration here, so be sure that the whole team working in the project agrees with them as well. In other words: use it with caution, because we certainly do.

This pattern holds almost all controllers in the last applications we built and we are quite happy with it. Be sure to grab our blog feed (see our sidebar), because we will continue writing about the Plataforma way in next posts!

Tags: , ,

This entry was posted on Monday, December 7th, 2009 at 2:55 pm and is filed under English. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

  • man, those are the supermodels of controllers, skinny as hell. nice post.
blog comments powered by Disqus