如何在一个 gem 中扩展 ApplicationController?

19

我想出了一个巧妙的方法,在Rails 3.x gem中扩展ApplicationController。

在我的gem的lib/my_namespace/my_controller.rb文件中,我写下了如下代码:

class MyNamespace::MyController < ApplicationController

  before_filter :some_method
  after_filter :another_method

  def initialize
    # getting classname of the subclass to use for lookup of the associated model, etc.
    # and storing the model_class in an instance variable
    # ...
  end

  # define :some_method, :another_method, etc.
  # ...

private
  attr_accessor :subclass_defined_during_initialize # etc.

  # etc.
end

但是当 Gem 加载时,app/controllers/application_controller.rb 还未加载,因此会失败:

/path/to/rvm/gemset/gems/activesupport-3.2.6/lib/active_support/dependencies.rb:251:
in `require': cannot load such file -- my_gem_name/application_controller (LoadError)

作为解决方法,我在我的宝石的lib/gem_namespace/application_controller.rb中定义了ApplicationController:
class ApplicationController < ActionController::Base
end

我假设即使我已经在那里定义它,在我的Rails 3应用程序的app/controllers/application_controller.rb中会重新定义它,这样扩展ApplicationController的应用程序中的两个控制器和扩展MyNamespace::MyController的控制器都将直接或间接地扩展在app/controllers/application_controller.rb中定义的ApplicationController。

然而,我们注意到在加载gem之后,扩展ApplicationController的控制器无法访问在app/controllers/application_controller.rb中定义的方法。此外,ApplicationHelper (app/helpers/application_helper.rb)模块不再被其他帮助程序模块加载。

如何在我的gem中的控制器中扩展ApplicationController,以定义before_filterafter_filter,并使用initialize访问类名来确定关联的模型类,然后可以在其方法中存储和使用?

更新2012/10/22:

这是我想出来的:

lib/your_gem_name/railtie.rb中:

module YourGemsModuleName
  class Railtie < Rails::Railtie
    initializer "your_gem_name.action_controller" do
    ActiveSupport.on_load(:action_controller) do
      puts "Extending #{self} with YourGemsModuleName::Controller"
      # ActionController::Base gets a method that allows controllers to include the new behavior
      include YourGemsModuleName::Controller # ActiveSupport::Concern
    end
  end
end

lib/your_gem_name/controller.rb 中:
module YourGemsModuleName
  module Controller
    extend ActiveSupport::Concern

    # note: don't specify included or ClassMethods if unused

    included do
      # anything you would want to do in every controller, for example: add a class attribute
      class_attribute :class_attribute_available_on_every_controller, instance_writer: false
    end

    module ClassMethods
      # notice: no self.method_name here, because this is being extended because ActiveSupport::Concern was extended
      def make_this_controller_fantastic
        before_filter :some_instance_method_available_on_every_controller # to be available on every controller
        after_filter :another_instance_method_available_on_every_controller # to be available on every controller
        include FantasticStuff
      end
    end

    # instance methods to go on every controller go here
    def some_instance_method_available_on_every_controller
      puts "a method available on every controller!"
    end

    def another_instance_method_available_on_every_controller
      puts "another method available on every controller!"
    end

    module FantasticStuff
      extend ActiveSupport::Concern

      # note: don't specify included or ClassMethods if unused

      included do
        class_attribute :class_attribute_only_available_on_fantastic_controllers, instance_writer: false
      end

      module ClassMethods
        # class methods available only if make_this_controller_fantastic is specified in the controller
        def some_fanastic_class_method
          put "a fantastic class method!"
        end
      end

      # instance methods available only if make_this_controller_fantastic is specified in the controller
      def some_fantastic_instance_method
        puts "a fantastic instance method!"
      end

      def another_fantastic_instance_method
        puts "another fantastic instance method!"
      end
    end
  end
end
4个回答

9
针对这种特定的功能,我建议在你的 gem 中创建一个模块,并将该模块包含在你的应用控制器中。
class ApplicationController < ActionController::Base
  include MyCoolModule
end

要添加前置过滤器等功能(将此代码添加到您的模块中)

def self.included(base)
  base.send(:before_filter, my_method)
end

更新:您可能只需执行base.before_filter:my_method,这更加简洁。

在这种情况下,包含一个模块真的有效吗?您能详细说明一下如何获取子类的名称,因为没有子类,以及如何利用 before_filter 和 after_filter 吗?请再仔细看一下问题,如果您能对此进行扩展,请澄清一下。否则,我会认为这不是一个选项。谢谢! - Gary S. Weaver
刚在问题的最后一行澄清了我需要使用 before_filter、after_filter,并访问子类的名称。 - Gary S. Weaver

5

这里是一个Gist,展示了如何访问子类的类并将其存储在实例变量中,并在before和after过滤器中访问它。它使用了include方法。


太棒了!包含一个模块确实是最好的主意。非常感谢你的帮助! - Gary S. Weaver

2

事实要简单得多,也更加灵活。

将以下内容添加到 lib/engine.rb 中:class Engine < Rails::Engine; end

然后只需要简单地使用:

ActionController::Base.class_eval do

  include SomethingFromMineGemModule

  # or:
  def hello_from_gem
    'Hey people!'
  end

end

0

我能够通过初始化回调引用ApplicationController。

宝石代码,它是ApplicationController的子类/引用:

class GemApplicationController < ApplicationController
  before_filter :method_to_call

  def method_to_call
    #your code here
  end
end

使用 gem 代码回调来创建子类化控制器:

module GemName
  def self.load_gem_application_controller
    require "path/to/gem_application_controller"
  end
end

rails_app/config/initializers/gem_name.rb

GemName.load_gem_application_controller

然后有控制器使用这个功能来继承GemApplicationController

class SpecialCaseController < GemApplicationController
  # this will inherit from the gem's controller, 
  # which inherits from the rails_app ApplicationController
end

1
我了解到最好使用模块。你只能直接从一个父类继承,但是你可以包含/扩展任意数量的模块。 - Gary S. Weaver
2
谢谢Gary。我最终也使用了一个模块来实现。 - t_itchy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接