动态命名空间控制器在Rails中具有回退功能

时间:2013-05-09 17:55:01

标签: ruby-on-rails ruby ruby-on-rails-3

我对新的Rails应用程序有一些奇怪的要求。我需要构建一个应用程序,其中所有路由都在多个名称空间中定义(让我解释一下)。我希望有一个应用程序,其中学校科目(数学,英语等)是名称空间:

%w[math english].each do |subject|
  namespace subject.to_sym do
    resources :students
  end
end

这很好,它可以工作,但它需要我为每个主题创建一个命名空间StudentsController,这意味着如果我添加一个新主题,那么我需要创建一个新的控制器。

我想要的是创建一个Base::StudentsController如果,假设存在Math::StudentsController那么它将被使用,如果它不存在,那么我们可以动态创建这个控制器并继承来自Base::StudentsController

这是可能的吗?如果是这样,那么我将如何实施呢?

6 个答案:

答案 0 :(得分:3)

以这种方式定义路线:

%w[math english].each do |subject|
  scope "/#{subject}" do
    begin
      "#{subject.camelcase}::StudentsController".constantize
      resources :students, controller: "#{subject}::students", only: :index
    rescue
      resources :students, controller: "base::students", only: :index
    end
  end
end

rake routes输出:

students GET /math/students(.:format)    base::students#index
         GET /english/students(.:format) english::students#index

如果出现english / students_controller.rb和math / students_controller。没有。

答案 1 :(得分:3)

我相信这样做会:

  %w[math english].each do |subject|
    namespace subject.to_sym do
      resources :students
    end
  end

  match ':subject/students(/:action(/:id))' => 'base/students'

使用这些组合路线,/math/students转到Math::StudentsController/english/students/转到English::StudentsController,以及所有其他主题(例如/physics/students和{{ 1}})转到/cs/students

我认为 完全 您想要什么,只需在原始解决方案中添加一行代码。

答案 2 :(得分:3)

重申您的要求:

  1. 每个主题/资源对的最小声明
  2. 使用专用控制器(Math::StudentsController)(如果存在),否则使用基本控制器(StudentsController
  3. Rails希望每个路由都有一个专用的控制器,并没有真正的方法来支持第二个要求。所以,我就是这样做的:

    Dynamicroutes::Application.routes.draw do
      SUBJECTS = [ "math", "english", "chemistry" ]
      RESOURCES = [ "assignments", "students" ]
    
      class DedicatedSubjectResourceControllerConstraint
        def initialize(subject, resource)
          @subject = subject
          @resource = resource
        end
    
        def matches?(request)
          begin
            defined?("#{@subject.capitalize}::#{@resource.capitalize}")
            return true
          rescue NameError
            Rails.logger.debug "No such class: #{@subject.capitalize}::#{@resource.capitalize}"
            return false
          end
        end
      end
    
      class ValidSubjectConstraint
        def matches?(request)
          return SUBJECTS.include?(request.path_parameters[:subject])
        end
      end
    
      SUBJECTS.each do |subject|
        RESOURCES.each do |resource|      
          namespace subject, :constraints => DedicatedSubjectResourceControllerConstraint.new(subject, resource) do
            resources resource
          end
        end
      end
    
      RESOURCES.each do |resource|
        scope "/:subject", :constraints => ValidSubjectConstraint.new do
          resources resource
        end
      end
    end
    

答案 3 :(得分:3)

这听起来像是const_missing的用途。如果你想做的是

  

创建Base :: StudentsController

     

如果,假设Math :: StudentsController存在

     

然后将使用

     

如果它不存在,那么我们可以动态创建这个控制器并继承自Base :: StudentsController

您可以通过添加动态常量查找(const_missing)和动态常量定义以及继承(Object.const_set)来实现这一点。

我想象这样的事情;通过一些调整和更严格的检查,可以工作:

# initializers/dynamic_controllers.rb

class ActionDispatch::Routing::RouteSet

  SUBJECTS = [ "math", "english", "chemistry" ]

  def const_missing(name, *args, &block)
    if SUBJECTS.any?{ |subject| name.include? subject.uppercase }
      Object.const_set name, Class.new(Base::StudentsController)
    else
      super
    end
  end

end

这将向ActionDispatch::Routing::RouteSet添加动态常量查找,Dynamicroutes::Application.routes继承,Dynamicroutes::Application.routes.draw中的未定义常量将生成从Base::StudentsController子类化的相应类。

答案 4 :(得分:1)

resourcesscope等所有路由助手都只是应用程序路由中的函数。您可以按如下方式定义自定义函数:

YourApplication.routes.draw do

  # Let's define a custom method that you're going to use for your specific needs
  def resources_with_fallback(*args, &block)
    target_module       = @scope[:module].camelize.constantize
    target_controller   = "#{args.first.to_s}_controller".camelize
    fallback_controller = args.last.delete(:fallback).to_s.camelize.constantize

    # Create the target controller class
    # using fallback_controller as the superclass
    # if it doesn't exist
    unless target_module.const_defined?(target_controller)
      target_module.const_set target_controller, Class.new(fallback_controller)
    end

    # Call original resources method
    resources *args, &block
  end

  # Now go ahead and define your routes!

  namespace "test" do
    namespace "new" do
      # Use our custom_resources function and pass a fallback parameter
      custom_resources :photos, :fallback => 'base/some_controller'
    end
  end

end

我在Rails 3.2中对此进行了测试,但它在所有3.x版本中同样适用。

我在任何地方都没有包含空检查或begin/rescue块。由于您将仅在需要时使用此自定义函数,我假设您将传递正确且必要的参数。如果你说你传递了一个不存在的fallback控制器,我宁愿路由解析因异常而失败,而不是试图处理它。

编辑:函数参数中的错误

编辑2:在函数参数中忘记&block

编辑3:将“_controller”添加到target_controller变量

答案 5 :(得分:1)

我最终在ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference中编写了一些自定义逻辑。我试图查找给定控制器所需的所有常量,如果它们丢失则创建它们。此代码 FAR 非常完美,因此请随时修改/改进。

class ActionDispatch::Routing::RouteSet::Dispatcher

  private

  def controller_reference(controller_param)
    const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"

    obj = Object
    const_name.split('::').each do |cn|
      begin
        obj =  obj.const_get(cn)
      rescue
        if obj == Object
          obj =  obj.const_set(cn, Class.new(ApplicationController))
        else
          puts "Creating #{obj}::#{cn} based on Generic::#{cn}"
          obj =  obj.const_set(cn, Class.new("Generic::#{cn}".constantize))
        end
      end
    end

    ActiveSupport::Dependencies.constantize(const_name)
  end

end