This is a tiny but useful tip, that saved many lines of repeated code in my controllers, hence why not share it :)
Do Not Repeat Thyself?
If you've looked at the controller code that's generated by scaffolding, you'll find something like this:
def show @user = User.find(params[:id]) end def new @user = User.new end def destroy User.find(params[:id]).destroy end
Sure, in this case all we are doing repeatedly is instantiating the user by a potentially available parameter value. What if instead we placed these common fetches in a controller filter, which would simply set an instance variable for us? Hell, we could even handle exceptions (such as invalid ID) in only one place this way! What not to like.
When dealing with a more complicated route that has been defined, this instantiation may become quite a bit more elaborate, and the case for a filter is even more justified.
Consider the case of building a collaboration system where you have projects and individual contributions under that project, as well as a producer of the project. We might want to support all project operations under a URL that looks kind of like this:
/user/kigster/projects/MyVacation/contribution/view/34
with a corresponding route in the config/routes.rb file:
# project management route map.connect '/user/:username/project/:project_name/contribution/:action/:id', :controller => "contribution"
Based on the route defined, Rails would create params[:username], params[:project_name] and params[:id] for contribution id, in addition to the standard action/controller pair.
Now imagine that the controller we are writing has many actions, such as add, edit, view, list, append, preview, post, comment, etc. All of them could use a handle on the project, it's producer and contribution instances. Ideally - in @project, @producer_user and @contribution respectively.
If we followed the scaffolding example, we'd simply add appropriate lookups at the beginning of each action. But that's a lot of repeated code!
So let's use filters instead, to get our common lookups under control.
class ContributeController < ApplicationController before_filter :setup_project def home # do stuff end private def setup_project @producer_user = User.find_by_username(params[:username]) @project = @producer_user.produced_projects.detect do |p| p.name.downcase.eql?(params[:project_name].downcase) end @contribution = @project.contributions.find_by_id(params[:id]) return true rescue Exception => e logger.info "can't show project:", e flash[:error] = e.message if e redirect_to :controller => "home" and return end end
This is short and elegant, and now every action in this controller (which is not excluded for setup_project filter) will have access to our instance variables. Groovy!
Issues
Of course nothing is free, and in this case we are potentially loosing performance. Perhaps some actions don't need to know the @producer_user. We'll be fetching it all the time, which saves time for us - developers. If performance problems occur because of extra unused fetches the filter can be broken up or optimized. Until then - it sure saves me a headache.
That's it!
Thoughts, comments?
Comments
before_filter :setup_project, :only => [:show, :edit, :update, :destroy]
they say:
"There’s a growing trend to abuse before_filter in controllers to load data for multiple actions. This is not what before_filter is for, and using it this way makes code harder to read and maintain as well as negating the benefits of controller action caching (and possibly other current or future Rails features).
As a rule, use before_filter only for actual filtering, and always use before_filter for code that does do filtering. When you want to factor out common initialization code, simply call the shared initialization method at the start of each action method that needs it."
Who's right?
Votes results below
Mac OS-X: 19 (54%)
Windows: 11 (31%)
Linux: 17 (48%)
FreeBSD: 1 (2%)
Total: 48 (135%)