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_filter
runs before the controller action - an
after_filter
runs after the controller action - an
around_filter
yields 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
end
Since 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
end
Wherever 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
end
Or 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...
end
Of 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...
end
If 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
end
Then in the ProductsController
:
class ProductsController < ApplicationController
include ResourceController
#...
end
Does 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