DRYing Up Polymorphic Controllers
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:
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:
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".
15 comments:
Very nice, looking for it for a long time.
Thanks a lot. That's exactly what I was looking for.
Good job.
you should now be able to do:
redirect_to url_for(@parent)
instead of
redirect_to send("#{ parent_type }_url", @parent)
(in edge)
There's another way that I picked up off the Rails mailing list a while ago.
It involves simply applying a regex to the URL in order to grab object class and id. I suppose that's kind of "cheating" but it's simple and gets the job done without a bunch of 'when' clauses.
I'd paste the code but this Google blog comment interface is terrible.
Really slick. Thanks for the tip.
guys this is very nice, but the route I took (no pun intended) was to standardize on variable name for node - parent node and then actually generate the routes in the model itself this seems more elegant and I guess will be the standard but it was easier to conceptualize & it's slightly more flexible by having them generate them in the model.
@nima: could you post an example of how you generated the routes in the model?
Using the above mentioned technique I am now running into problems with my view when calling paths because of nested routes needed to have the parent_type prepended to every path.
For example- map.resources client, :has_many => :addresses
map.resources contact, :has_many => :addresses
now the view requires client_address_path or contact_address_path (instead of just address_path)
but I can't access parent_type from the view for use on a send("#{parent_type}"_address_path)
anyone have a suggestion on a way around this? Am I overlooking a clever use of url_for ?
jmcopeland: if you on edge, you can use url_for([@parent, @address]). Otherwise, just pass parent_type and parent_obj via @ from the controller to a view so you can use send("#{ parent_type }_address_path", @parent_obj, @address). Hope it helps.
@val: thanks for help, I wasn't aware url_for took an array. Is there a way to use url_for with nested routes and an edit or create action? perhaps tacking on a :method => post to the url or something? If possible I would prefer to avoid all the send("#{ @parent_type }_new_address_path", @parent_obj) calls in the view.
In the view I need the client_new_address_path and client_edit_address_path.
I guess what I really need is a way to generate all new routes with parent_address_path instead of the client_address_path or contact_address_path... hmmm... not exactly sure what i want to do with this... these nested polymorphic routes are a pain.
According to Ryan D's blog url_for is strictly for internal Rails usage only. As for my technique its very simple just standardize your parent and child instance variable names and they're method names.
Then generate the Restful routes just like RAILS expects them I.E. edit = /some_url;edit?_method=get. Sure its not pretty but it sure flows nice when you are programming
Nice. I'm currently working with polymorphic controllers and trying to find ways to tidy things up. This helped.
Your class instance variable doesn't inherit, though, so I did this instead:
class_inheritable_accessor :parents
def self.parent_resources(*parents)
self.parents = parents
end
Oh, and I did
def parent
return @parent if @parent
@parent = parent_class && parent_class.find_by_id(parent_id(parent_type))
instance_variable_set("@#{parent_type}", @parent) # Layouts usually want a @user, @item etc
end
to keep down on DB hits (or is Rails smart enough these days to do that anyway?) and for the reason mentioned in the comment.
Have you looked at the ResourceFu plugin? I'm using it for polymorphic attendees to both webinars and training courses. I haven't found any documentation outside the readme but it works really well once you understand it.
Take a look at http://dev.rubyonrails.org/ticket/9932
map.resources :posts, :polymorphic => [:events, :articles] do |posts|
posts.resources :assets, :polymorphic => [:images, :movies]
end
edit_post_asset_path(:post_type => 'events', :post_id =>
1, :type => 'movies', :id => 2)
=> "/events/1/movies/2/edit"
Useful, isn't it?
My users can have one (has_one) blog and I want to do something like this:
/users/1/blog/posts
But this doesn't work with this code 'cause I can't obtain the blog ID using parent_id from the Posts controller.
Any clue on how to extend this to obtain the blog ID?
Post a Comment