Polymorphic resources, de uma forma DRY
Maio 25, 2008 @ 03:00 PM

Modelo polimórfico
Um modelo polimórfico pode pertencer a mais de uma classe modelo pai. Por exemplo, Comments pode pertencer a Articles e Documents.
Para isso teriamos um campo string chamado type em nossa tabela comments. Esse campo é chave no Rails e é preenchido com a classe do modelo pai num relacionamento polimórfico.
Nossos modelos ficariam como abaixo:
1 2 3 4 5 6 7 8 9 10 11 |
class Article < ActiveRecord::Base has_many :comments, :as => :commentable end class Document < ActiveRecord::Base has_many :comments, :as => :commentable end class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end |
Dessa maneira, você pode referenciar article.comments e document.comments para seus objetos Article e Document, respectivamente.
Rotas para nosso controlador polimórfico
Agora temos nossos modelos Article e Document possuindo o mesmo modelo Comment. Definindo nossas rotas, veremos que ele será polimórfico também, pois vai estar aninhado aos controladores Articles e Documents .
Abaixo as rotas:
1 2 3 4 |
ActionController::Routing::Routes.draw do |map| map.resources :articles, :has_many => [ :comments ] map.resources :documents, :has_many => [ :comments ] end |
Abaixo exemplo de alguns itens gerados a partir dessas duas rotas:
1 2 3 4 5 |
article_comment_path(@article,@comment) # => /articles/1/comments/22 article_new_comment_path(@article) # => /articles/1/comments/new document_comment_path(@document,@comment) # => /documents/1/comments/22 document_new_comment_path(@document) # => /documents/1/comments/new |
Controlador polimórfico
Através das novas rotas, vimos que um comentário poderia ser criado a partir de /articles/1/comments/new ou /documents/1/comments/new. Podemos fazer com que nosso controlador gerencie isso de uma forma simples, como abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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[:document_id] then Document.find_by_id(params[:document_id]) end end def parent_url(parent) case when params[:article_id] then article_url(parent) when params[:document_id] then document_url(parent) end end end |
Faz o que precisa? Faz. Mas isso pode ser mais bonito. Dessa forma, sempre que adicionar-mos um item que pode receber um comentário, ele deverá criar uma nova condição em nossos case. Para isso, vamos passar todo esse código de forma genérica para nosso ApplicationController.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class CommentsController < ApplicationController parent_resources :article, :document 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 |
Essa solução foi publicada inicialmente por Val Aleksenko, em seu blog Revolution On Rails. Através dela, criei um plugin chamado parent_resources.
Isso será adicionado ao Edge e já tem um patch sendo desenvolvido. Então esse plugin tem um tempo bem curto de vida. Mas a solução me salvou de várias repetições de código. Fica ae para o caso de alguem precisar.



Desculpe, os comentários para esse artigo estão fechados devido ao número de SPAMs que tenho recebido.