开发者

RoR: how can i create a an object in a controller whose class is determined dynamically?

My app has an STI model:

开发者_高级运维# file: app/models/metered_service.rb
class MeteredService < ActiveRecord::Base
  ...
end
# file: app/models/metered_services/pge_residential.rb
class PGEResidential < MeteredService
  ...
end
# file: app/models/metered_services/sce_residential.rb
class SCEResidential < MeteredService
  ...
end

and a schema that support STI:

# file: db/schema.rb
create_table "metered_services", :force => true do |t|
  t.integer  "premise_id"
  t.string   "type"
end

MeteredService is a nested resource (though that's not really relevant to this question):

# file: config/routes.rb
resources :premises do
  resources :metered_services    
end

So here's the deal: To create a MeteredService, the user chooses one of its many sub-classes in a pulldown list. The form returns the class name to the MeteredServicesController#create in params['metered_services']['class'] as a string. Now we need to create the proper subclass.

The approach I'm taking works - sort of - but I'm wondering if this is the best way:

def create
  @premise = Premise.find(params[:premise_id])
  MeteredService.descendants()  # see note
  class_name = params["metered_service"].delete("class")
  @metered_service = Object.const_get(class_name).new(params[:metered_service].merge({:premise_id => @premise.id}))
  if @metered_service.save
    ... standard endgame
  end
end

What I'm doing is stripping the class name out of params['metered_service'] so I can use the remaining parameters to create the metered service. And the class_name is resolved to a class (via Object.const_get) so I can call the .new method on it.

The MeteredServices.descendants() call is there because of the way caching is done in development mode. It works, but it's really ugly - see this question for an explanation of why I'm doing it.

Is there a better / more reliable way to do this?


As John Gibb said in his comment, your main problem is security. You must filter the classes through your approved white list.

The solution you gave in your comment is also not perfect. First an instance of MeteredService is created, and then you just change a text property with a name of another class. The instance you are working with is still the base class. This may lead to some problems if you, for example, define some validations on the descending class.

Do something like this:

AVAILABLE_CLASSES = {"PGEResidential" => PGEResidential,
                     "SCEResidential" => SCEResidential } # You may automatize this

def create
  #....
  class_name = params["metered_service"].delete("class")
  if c = AVAILABLE_CLASSES[class_name]
    @metered_service = c.new(params[:met...
  else
    handle_error_somehow
  end
  ...
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜