RoR: как я могу создать объект в контроллере, класс которого определяется динамически?

Мое приложение имеет модель STI:

# 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

и схема, поддерживающая STI:

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

MeteredService — это вложенный ресурс (хотя это не совсем относится к этому вопросу):

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

Итак, вот в чем дело: чтобы создать MeteredService, пользователь выбирает один из его многочисленных подклассов в раскрывающемся списке. Форма возвращает имя класса в MeteredServicesController#create в params['metered_services']['class'] в виде строки. Теперь нам нужно создать правильный подкласс.

Подход, который я использую, работает - вроде как - но мне интересно, лучший ли это способ:

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

Что я делаю, так это удаляю имя класса из params['metered_service'], чтобы я мог использовать оставшиеся параметры для создания измеряемой услуги. И class_name разрешается в класс (через Object.const_get), поэтому я могу вызвать для него метод .new.

Вызов MeteredServices.descendants() существует из-за способа кэширования в режиме разработки. Это работает, но это действительно уродливо - см. вопрос для объяснения того, почему я это делаю.

Есть ли лучший/надежный способ сделать это?


person fearless_fool    schedule 25.05.2011    source источник
comment
Будьте осторожны при вызове const_get при вводе данных пользователем. теоретически они могли пройти во что угодно. Может быть, вы могли бы фильтровать с помощью белого списка?   -  person John Gibb    schedule 25.05.2011
comment
Я только что понял, что, хотя я не могу сделать MeteredService.new(:type => 'PGEResidential'), я могу сделать MeteredService.new(...) {|m| m.type = 'PGEResidential'}, что (удивительно) кажется правильным и (кивок на @John Gibb) оказывается безопасным, поскольку тип проверяется при сохранении.   -  person fearless_fool    schedule 25.05.2011


Ответы (1)


Как сказал в своем комментарии Джон Гибб, ваша главная проблема — это безопасность. Вы должны отфильтровать классы через утвержденный белый список.

Решение, которое вы дали в своем комментарии, также не идеально. Сначала создается экземпляр MeteredService, а затем вы просто меняете текстовое свойство на имя другого класса. Экземпляр, с которым вы работаете, по-прежнему является базовым классом. Это может привести к некоторым проблемам, если вы, например, определите некоторые проверки для нисходящего класса.

Сделайте что-то вроде этого:

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
  ...
person Arsen7    schedule 25.05.2011
comment
давая вам чек, так как ваш подход будет работать. В итоге я отказался от модели STI. MeteredService теперь является обычной моделью, а то, что раньше было подклассами, теперь является простыми (не AR) классами. Спасает от многих головных болей. - person fearless_fool; 19.07.2011