Quem sou eu

Recommend Me

Meu nome é Davis Zanetti Cabral, tenho 23 anos, sou muito bem casado, tenho uma filha linda, sou programador e atuo na área de Desenvolvimento Web há mais de 8 anos. Atualmente trabalho em casa e presto serviços em Ruby on Rails, PHP e xHTML/CSS. Saiba mais

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.