Clean up your controllers with inherited resources

Hot vacancies! Apply to a job of your dream!

If you haven’t noticed yet that main rule in rails development says do not complicate logic in controller I recommend you to read these articles:

This post assumes that you’ve already known this truth and it’s not necessary to clarify it for you. Also I assume that you know what is REST and those idea that you should follow RESTful rules. I will show you how to avoid writting boring repetitive code in controllers.

Boring code

Imagine you have controller:

class ChannelsController < ApplicationController
  def index
    @channels = current_user.channels
    respond_with(@channels)
  end

  def new
    @channel = current_user.channels.build
    respond_with(@channel)
  end

  def create
    @channel = current_user.channels.build(params[:channel])
    @channel.save
    respond_with(@channel)
  end

  def show
    @channel = current_user.channels.find(params[:id])
    respond_with(@channel)
  end

  def edit
    @channel = current_user.channels.find(params[:id])
    respond_with(@channel)
  end

  def update
    @channel = current_user.channels.find(params[:id])
    @channel.update_attributes(params[:channel])
    respond_with(@channel)
  end

  private
  def current_user
    @current_user ||= User.find(params[:user_id])
  end
end

This code is looking pretty nice. But it contains a lot of repetitive code such as: current_user.channels.find(params[:id]), respond_with and so on. Of cource you can write before filters to avoid this collision like this:

class ChannelsController < ApplicationController
  before_filter :find_channel, :only => [:edit, :show, :update]

  def index
    @channels = current_user.channels
    respond_with(@channels)
  end

  def new
    @channel = current_user.channels.build
    respond_with(@channel)
  end

  def create
    @channel = current_user.channels.build(params[:channel])
    @channel.save
    respond_with(@channel)
  end

  def show
    respond_with(@channel)
  end

  def edit
    respond_with(@channel)
  end

  def update
    @channel.update_attributes(params[:channel])
    respond_with(@channel)
  end

  private
  def find_channel
    @channel = current_user.channels.find(params[:id])
  end

  def current_user
    @current_user ||= User.find(params[:user_id])
  end
end

Looks beter, yes? But it’s possible to write this functionality in 3 (!!!) lines at all. Check it out:

class ChannelsController < InheritedResources::Base
  belong_to :user
end

So, there is no any boring code, it’s really clean controller and you can fill confident that it doesn’t contain any bug, your views work as before and you don’t have to change there anything. If you are interested in it just insert this line in your Gemfile:

gem 'inherited_resources'

and that’s it. Pay attention that you have to inherit your controllers not from ApplicationController but from InheritedResources::Base.

Complicated logic

May be you’ve paid attention that ChannelsController had private method:

def current_user
  @current_user ||= User.find(params[:user_id])
end

which gets current_user for each request, also this controller requires :user_id param. This controller listens to following routes:

       user_channels GET    /users/:user_id/channels(.:format)              channels#index
                     POST   /users/:user_id/channels(.:format)              channels#create
    new_user_channel GET    /users/:user_id/channels/new(.:format)          channels#new
   edit_user_channel GET    /users/:user_id/channels/:id/edit(.:format)     channels#edit
        user_channel GET    /users/:user_id/channels/:id(.:format)          channels#show
                     PUT    /users/:user_id/channels/:id(.:format)          channels#update
                     DELETE /users/:user_id/channels/:id(.:format)          channels#destroy

But what if you don’t want to pass :user_id to every call and all channels should be in current_user scope? Here current_user is a helper method in ApplicationController which holds signed in user. It’s not problem for inherited_resources gem too:

class ChannelsController < InheritedResources::Base

  protected
  def begin_of_association_chain
    @begin_of_association_chain ||= current_user
  end
end

It will be enough to get working contoller for our demands. Now you can change your routes:

       user_channels GET    /user/channels(.:format)              channels#index
                     POST   /user/channels(.:format)              channels#create
    new_user_channel GET    /user/channels/new(.:format)          channels#new
   edit_user_channel GET    /user/channels/:id/edit(.:format)     channels#edit
        user_channel GET    /user/channels/:id(.:format)          channels#show
                     PUT    /user/channels/:id(.:format)          channels#update
                     DELETE /user/channels/:id(.:format)          channels#destroy

Let’s complicate logic and rework our controller to listen to both variants of routes:

       user_channels GET    /user/channels(.:format)              channels#index
                     POST   /user/channels(.:format)              channels#create
    new_user_channel GET    /user/channels/new(.:format)          channels#new
   edit_user_channel GET    /user/channels/:id/edit(.:format)     channels#edit
        user_channel GET    /user/channels/:id(.:format)          channels#show
                     PUT    /user/channels/:id(.:format)          channels#update
                     DELETE /user/channels/:id(.:format)          channels#destroy
       user_channels GET    /users/:user_id/channels(.:format)              channels#index
                     POST   /users/:user_id/channels(.:format)              channels#create
    new_user_channel GET    /users/:user_id/channels/new(.:format)          channels#new
   edit_user_channel GET    /users/:user_id/channels/:id/edit(.:format)     channels#edit
        user_channel GET    /users/:user_id/channels/:id(.:format)          channels#show
                     PUT    /users/:user_id/channels/:id(.:format)          channels#update
                     DELETE /users/:user_id/channels/:id(.:format)          channels#destroy

How to change our ChannelController? Very simple:

class ChannelsController < InheritedResources::Base

  protected
  def begin_of_association_chain
    @begin_of_association_chain ||= (User.find_by_id(params[:user_id]) || current_user) || raise(ActiveRecord::RecordNotFound)
  end
end

This way we have universal RESTful controller and we fit it in 6 lines of code instead of 100 or may be 200.

Resources

Check out InheritedResources gem and read its README. I promise you will find there a lot of interesting information which I’ve omited in my post. Also README is not contained information for all possibilities, so I would recommend you to read sources.

I hope you don’t blame me for your spent time. Thank you for your reading!

PS. InheritedResources is compatible with cacan. You have to add one line code load_and_authorize_resource and your restrictions will work exactly how you want.

Hot vacancies! Apply to a job of your dream!