The Rails REST implementation dictates the default seven actions for your controllers, but frequently we want to share functionality across multiple actions or even across controllers. Controller filters are the easiest way to do that.
There are three types of filters implemented in Rails:
- a
before_filterruns before the controller action - an
after_filterruns after the controller action - an
around_filteryields to the controller action wherever it chooses
before_filter is, by far, the most common. There are two ways to invoke a before filter. First, as an anonymous block:
class ProductsController < ApplicationController
before_filter do
@product = Product.find(params[:id]) if params[:id]
end
#...Or, preferably, as a named method:
class ProductsController < ApplicationController
before_filter :load_product
# Actions...
private
def load_product
@product = Product.find(params[:id]) if params[:id]
end
endSince the filter is only going to be used within the controller, and won't be accessed directly by the router, it's good form to make the method private.
An after_filter works exactly the same, so it's not worth another example.
I've never needed an around filter and I always see the same pattern in examples of its usage:
around_filter :wrap_actions
def wrap_actions
begin
yield
rescue
render :text => "It broke!"
end
endWherever yield is called, the action will be executed. So the functionality here could recover from some exception that occurs in the yielded action(s).
All three filters accept the options :only and a list of actions for which the filter should run or :except and a list of actions for which the filter should not run.
For example, we could remove the condition from the before_filter sample above:
class ProductsController < ApplicationController
before_filter :load_product, :only => [:show, :edit, :update, :destroy]
# Actions...
private
def load_product
@product = Product.find(params[:id])
end
endOr get the same effect using :except:
class ProductsController < ApplicationController
before_filter :load_product, :except => [:index, :new, :create]
#...Filters are most often about sharing code across actions in a controller, but why not share them across controllers?
The most common way to reuse filters across controllers is to move them to ApplicationController. Since all controllers inherit from ApplicationController, they'll have access to those methods. For example:
class ApplicationController < ActionController::Base
protect_from_forgery
private
def load_product
@product = Product.find(params[:id])
end
end
class ProductsController < ApplicationController
before_filter :load_product, :only => [:show, :edit, :update, :destroy]
# Actions...
endOf course, we have to question how useful that method will be, relying on params[:id], in other controllers.
As an experiment, I wanted to try writing a general resource lookup that would figure out which model to lookup based on the current controller name. Here are the results after some metaprogramming trickeration:
class ApplicationController < ActionController::Base
protect_from_forgery
private
def find_resource
class_name = params[:controller].singularize
klass = class_name.camelize.constantize
self.instance_variable_set "@" + class_name, klass.find(params[:id])
end
end
class ProductsController < ApplicationController
before_filter :find_resource, :only => [:show, :edit, :update, :destroy]
# Actions...
endIf several controllers share a common logical abstraction, it might make sense to have them share a module of filters and other common code. For instance, this module could be defined in application_controller.rb or in it's own app/controllers/resource_controller.rb file:
module ResourceController
extend ActiveSupport::Concern
included do
before_filter :find_resource, :only => [:show, :edit, :update, :destroy]
end
module InstanceMethods
def find_resource
class_name = params[:controller].singularize
klass = class_name.camelize.constantize
self.instance_variable_set "@" + class_name, klass.find(params[:id])
end
end
endThen in the ProductsController:
class ProductsController < ApplicationController
include ResourceController
#...
endDoes this encapsulate the common concerns or obfuscate the use of the before_filter? You have to be the judge for your application.
[TODO: Add Exercises]
- Rails Guide on Controller Filters: http://guides.rubyonrails.org/action_controller_overview.html#filters
Good one