Polymorphic routes allow drying up the controller implementation when functionality is identical, regardless of entry point. A good example is comments for articles and blogs. There is a challenge to balance the implementation of the comments controller reflecting the multiple incoming routes. Let's look at the way it could be written.
Routing is straightforward with blogs and article models acting as commentable and both the comment model and comment controllers being polymorphic:
ActionController::Routing::Routes.draw do |map|
map.resources :articles, :has_many => [ :comments ]
map.resources :blogs, :has_many => [ :comments ]
end
This means that a comment can be created via post to either
/articles/1/comments/new or
/blogs/1/comments/new. The comments controller can be implemented to handle both:
class CommentsController < ApplicationController
def new
@parent = parent_object
@comment = Comment.new
end
def create
@parent = parent_object
@comment = @parent.comments.build(params[:comment])
if @comment.valid? and @comment.save
redirect_to parent_url(@parent)
else
render :action => 'new'
end
end
private
def parent_object
case
when params[:article_id] then Article.find_by_id(params[:article_id])
when params[:news_id] then News.find_by_id(params[:news_id])
end
end
def parent_url(parent)
case
when params[:article_id] then article_url(parent)
when params[:news_id] then news_url(parent)
end
end
end
This method works fine and there is not much drive to start refactoring it right away. This changes, though, if there is a need to add another commentable or allow some other polymorphic route. Instead of adding more 'when' clauses the whole functionality can be extracted and abstracted based on the idea of having fixed naming conventions for resources that allow movement from a controller name to a model. The refactored example has the parent functionality extracted to the application controller to share it as-is with other polymorphic routes:
class ApplicationController < ActionController::Base
protected
class << self
attr_reader :parents
def parent_resources(*parents)
@parents = parents
end
end
def parent_id(parent)
request.path_parameters["#{ parent }_id"]
end
def parent_type
self.class.parents.detect { |parent| parent_id(parent) }
end
def parent_class
parent_type && parent_type.to_s.classify.constantize
end
def parent_object
parent_class && parent_class.find_by_id(parent_id(parent_type))
end
end
class CommentsController < ApplicationController
parent_resources :article, :blogs
def new
@parent = parent_object
@comment = Comment.new
end
def create
@parent = parent_object
@comment = @parent.comments.build(params[:comment])
if @comment.valid? and @comment.save
redirect_to send("#{ parent_type }_url", @parent)
else
render :action => 'new'
end
end
end
The
parent_resources call declares resources that are parent for a current controller. An alternative approach is to guess such parent resources from the request URI and routes. Aaron is currently working on a patch on Edge implementing it. We'll update this post later.
If you currently use multiple polymorphic resources and have
if clauses in the controller code, you might want to rethink how it could be DRYed up using this approach. In some cases views are very parent type specific. Then it might be better to have different templates and partials rendered via
render :template => "/controller/#{ parent_type }_action".