I just tried out some simple filters in Rails, and am blown away. Up until now, I'd only embraced those features of Rails (like ActiveRecord) that allowed me to omit steps in the application without actually adding any code on my part. Filters are a bit different — you have to identify patterns in your data flow and abstract them out into steps that will be called before (or after) each action. These calls are defined within class definitions, rather than called explicitly by the action methods themselves.
Using Filters to Authenticate
Filters are called filters because they return a Boolean, and if that return value is (Updated 2010: see Errata!)
false, the action is never called. You can use the the
logged_in? method of
:acts_as_authenticated to prohibit access to non-users — just add
before_filter :logged_in? to your controller class and you're set!
Using Filters to Load Data
Today I refactored all my controllers to depend on a
before_filter to load their objects. Beforehand, each action followed a predictable pattern:
- Read an ID from the parameters
- Load the object for that ID from the database
- If that object was the child of an object I also needed, load the parent object as well
In some cases, loading the relevant objects was the only work a controller action did.
To eliminate this, I added a very simple method to my
@work = Work.find(params[:work_id])
@page = Page.find(params[:page_id])
@work = @page.work
I then added
before_filter :load_objects_from_params to the class definition and removed all the places in my subclasses where I was calling
find on either
The result was an overall 7% reduction in lines of code -- redundant, error-prone lines of code, at that!
Line counts before the refactoring:
In the case of my (rather bare-bones)
DisplayController, the entire contents of the class has been eliminated!
Perhaps best of all is the effect this has on my authentication code. Since I track object-level permissions, I have to read the object the user is acting upon, check whether or not they're the owner, then decide whether to reject the attempt. When the objects may be of different types in the same controller, this can get a bit hairy:
if ['new', 'create'].include? action_name
# test if the work is owned by the current user
work = Work.find(params[:work_id])
return work.owner == current_user
# is the page owned by the current user
page = Page.find(params[:page_id])
return page.work.owner == current_user
After refactoring my
ApplicationController to call my
load_objects_from_params method, this becomes:
return @work.owner == current_user