Skip to content

Instantly share code, notes, and snippets.

@jcasimir
Created July 22, 2011 18:14
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jcasimir/1100026 to your computer and use it in GitHub Desktop.
Save jcasimir/1100026 to your computer and use it in GitHub Desktop.
Controller Filters

Controller Filters

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.

Before, After, and Around

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

Basic Usage

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.

after_filter

An after_filter works exactly the same, so it's not worth another example.

around_filter

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).

only and except

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]
  #...

Sharing Filters

Filters are most often about sharing code across actions in a controller, but why not share them across controllers?

Drop Them in ApplicationController

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

Controller Module

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.

Exercises

[TODO: Add Exercises]

References

@mike1011
Copy link

mike1011 commented Oct 1, 2018

Good one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment